Merge pull request #126 from britiscurious/master
added --export-as-hardlink option
This commit is contained in:
@@ -201,6 +201,7 @@ Options:
|
|||||||
Search by end item date, e.g.
|
Search by end item date, e.g.
|
||||||
2000-01-12T12:00:00 or 2000-12-31 (ISO 8601
|
2000-01-12T12:00:00 or 2000-12-31 (ISO 8601
|
||||||
w/o TZ).
|
w/o TZ).
|
||||||
|
--export-as-hardlink Hardlink files instead of copying them.
|
||||||
--overwrite Overwrite existing files. Default behavior
|
--overwrite Overwrite existing files. Default behavior
|
||||||
is to add (1), (2), etc to filename if file
|
is to add (1), (2), etc to filename if file
|
||||||
already exists. Use this with caution as it
|
already exists. Use this with caution as it
|
||||||
@@ -984,12 +985,13 @@ Returns True if photo is a panorama, otherwise False.
|
|||||||
Returns a JSON representation of all photo info
|
Returns a JSON representation of all photo info
|
||||||
|
|
||||||
#### `export()`
|
#### `export()`
|
||||||
`export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
|
`export(dest, *filename, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
|
||||||
|
|
||||||
Export photo from the Photos library to another destination on disk.
|
Export photo from the Photos library to another destination on disk.
|
||||||
- dest: must be valid destination path as str (or exception raised).
|
- dest: must be valid destination path as str (or exception raised).
|
||||||
- *filename (optional): name of picture as str; if not provided, will use current filename. **NOTE**: if provided, user must ensure file extension (suffix) is correct. For example, if photo is .CR2 file, edited image may be .jpeg. If you provide an extension different than what the actual file is, export will print a warning but will happily export the photo using the incorrect file extension. e.g. to get the extension of the edited photo, look at [PhotoInfo.path_edited](#path_edited).
|
- *filename (optional): name of picture as str; if not provided, will use current filename. **NOTE**: if provided, user must ensure file extension (suffix) is correct. For example, if photo is .CR2 file, edited image may be .jpeg. If you provide an extension different than what the actual file is, export will print a warning but will happily export the photo using the incorrect file extension. e.g. to get the extension of the edited photo, look at [PhotoInfo.path_edited](#path_edited).
|
||||||
- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
|
- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
|
||||||
|
- export_as_hardlink: boolean; if True (default=False), will hardlink files instead of copying them
|
||||||
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
|
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
|
||||||
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
|
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
|
||||||
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
|
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class ExportCommand(click.Command):
|
|||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"The templating system may also be used with the --keyword-template option "
|
"The templating system may also be used with the --keyword-template option "
|
||||||
+ "to set keywords on export (with --exiftool or --sidecar), "
|
+ "to set keywords on export (with --exiftool or --sidecar), "
|
||||||
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
|
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
|
||||||
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"'
|
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"'
|
||||||
@@ -860,6 +860,11 @@ def query(
|
|||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||||
@query_options
|
@query_options
|
||||||
|
@click.option(
|
||||||
|
"--export-as-hardlink",
|
||||||
|
is_flag=True,
|
||||||
|
help="Hardlink files instead of copying them. ",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--overwrite",
|
"--overwrite",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -1004,6 +1009,7 @@ def export(
|
|||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
verbose,
|
verbose,
|
||||||
|
export_as_hardlink,
|
||||||
overwrite,
|
overwrite,
|
||||||
export_by_date,
|
export_by_date,
|
||||||
skip_edited,
|
skip_edited,
|
||||||
@@ -1195,6 +1201,7 @@ def export(
|
|||||||
verbose,
|
verbose,
|
||||||
export_by_date,
|
export_by_date,
|
||||||
sidecar,
|
sidecar,
|
||||||
|
export_as_hardlink,
|
||||||
overwrite,
|
overwrite,
|
||||||
export_edited,
|
export_edited,
|
||||||
original_name,
|
original_name,
|
||||||
@@ -1216,6 +1223,7 @@ def export(
|
|||||||
verbose,
|
verbose,
|
||||||
export_by_date,
|
export_by_date,
|
||||||
sidecar,
|
sidecar,
|
||||||
|
export_as_hardlink,
|
||||||
overwrite,
|
overwrite,
|
||||||
export_edited,
|
export_edited,
|
||||||
original_name,
|
original_name,
|
||||||
@@ -1605,6 +1613,7 @@ def export_photo(
|
|||||||
verbose,
|
verbose,
|
||||||
export_by_date,
|
export_by_date,
|
||||||
sidecar,
|
sidecar,
|
||||||
|
export_as_hardlink,
|
||||||
overwrite,
|
overwrite,
|
||||||
export_edited,
|
export_edited,
|
||||||
original_name,
|
original_name,
|
||||||
@@ -1624,6 +1633,7 @@ def export_photo(
|
|||||||
verbose: boolean; print verbose output
|
verbose: boolean; print verbose output
|
||||||
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
||||||
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
||||||
|
export_as_hardlink: boolean; hardlink files instead of copying them
|
||||||
overwrite: boolean; overwrite dest file if it already exists
|
overwrite: boolean; overwrite dest file if it already exists
|
||||||
original_name: boolean; use original filename instead of current filename
|
original_name: boolean; use original filename instead of current filename
|
||||||
export_live: boolean; also export live video component if photo is a live photo
|
export_live: boolean; also export live video component if photo is a live photo
|
||||||
@@ -1715,6 +1725,7 @@ def export_photo(
|
|||||||
sidecar_xmp=sidecar_xmp,
|
sidecar_xmp=sidecar_xmp,
|
||||||
live_photo=export_live,
|
live_photo=export_live,
|
||||||
raw_photo=export_raw,
|
raw_photo=export_raw,
|
||||||
|
export_as_hardlink=export_as_hardlink,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
exiftool=exiftool,
|
exiftool=exiftool,
|
||||||
@@ -1752,6 +1763,7 @@ def export_photo(
|
|||||||
edited_name,
|
edited_name,
|
||||||
sidecar_json=sidecar_json,
|
sidecar_json=sidecar_json,
|
||||||
sidecar_xmp=sidecar_xmp,
|
sidecar_xmp=sidecar_xmp,
|
||||||
|
export_as_hardlink=export_as_hardlink,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
edited=True,
|
edited=True,
|
||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.28.13"
|
__version__ = "0.28.14"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from .template import (
|
|||||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
_hardlink_file,
|
||||||
_copy_file,
|
_copy_file,
|
||||||
_export_photo_uuid_applescript,
|
_export_photo_uuid_applescript,
|
||||||
_get_resource_loc,
|
_get_resource_loc,
|
||||||
@@ -635,6 +636,7 @@ class PhotoInfo:
|
|||||||
edited=False,
|
edited=False,
|
||||||
live_photo=False,
|
live_photo=False,
|
||||||
raw_photo=False,
|
raw_photo=False,
|
||||||
|
export_as_hardlink=False,
|
||||||
overwrite=False,
|
overwrite=False,
|
||||||
increment=True,
|
increment=True,
|
||||||
sidecar_json=False,
|
sidecar_json=False,
|
||||||
@@ -660,6 +662,7 @@ class PhotoInfo:
|
|||||||
(or raise exception if no edited version)
|
(or raise exception if no edited version)
|
||||||
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
|
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
|
||||||
raw_photo: (boolean, default=False); if True, will also export the associted RAW photo
|
raw_photo: (boolean, default=False); if True, will also export the associted RAW photo
|
||||||
|
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
||||||
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
|
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
|
||||||
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
||||||
if overwrite=False and increment=False, export will fail if destination file already exists
|
if overwrite=False and increment=False, export will fail if destination file already exists
|
||||||
@@ -802,7 +805,10 @@ class PhotoInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# copy the file, _copy_file uses ditto to preserve Mac extended attributes
|
# copy the file, _copy_file uses ditto to preserve Mac extended attributes
|
||||||
_copy_file(src, dest, norsrc=no_xattr)
|
if export_as_hardlink:
|
||||||
|
_hardlink_file(src, dest)
|
||||||
|
else:
|
||||||
|
_copy_file(src, dest, norsrc=no_xattr)
|
||||||
exported_files.append(str(dest))
|
exported_files.append(str(dest))
|
||||||
|
|
||||||
# copy live photo associated .mov if requested
|
# copy live photo associated .mov if requested
|
||||||
@@ -814,7 +820,10 @@ class PhotoInfo:
|
|||||||
logging.debug(
|
logging.debug(
|
||||||
f"Exporting live photo video of {filename} as {live_name.name}"
|
f"Exporting live photo video of {filename} as {live_name.name}"
|
||||||
)
|
)
|
||||||
_copy_file(src_live, str(live_name), norsrc=no_xattr)
|
if export_as_hardlink:
|
||||||
|
_hardlink_file(src_live, str(live_name))
|
||||||
|
else:
|
||||||
|
_copy_file(src_live, str(live_name), norsrc=no_xattr)
|
||||||
exported_files.append(str(live_name))
|
exported_files.append(str(live_name))
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Skipping missing live movie for {filename}")
|
logging.warning(f"Skipping missing live movie for {filename}")
|
||||||
@@ -828,7 +837,10 @@ class PhotoInfo:
|
|||||||
logging.debug(
|
logging.debug(
|
||||||
f"Exporting RAW photo of {filename} as {raw_name.name}"
|
f"Exporting RAW photo of {filename} as {raw_name.name}"
|
||||||
)
|
)
|
||||||
_copy_file(str(raw_path), str(raw_name), norsrc=no_xattr)
|
if export_as_hardlink:
|
||||||
|
_hardlink_file(str(raw_path), str(raw_name))
|
||||||
|
else:
|
||||||
|
_copy_file(str(raw_path), str(raw_name), norsrc=no_xattr)
|
||||||
exported_files.append(str(raw_name))
|
exported_files.append(str(raw_name))
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Skipping missing RAW photo for {filename}")
|
logging.warning(f"Skipping missing RAW photo for {filename}")
|
||||||
|
|||||||
@@ -118,6 +118,30 @@ def _dd_to_dms(dd):
|
|||||||
return int(deg_), int(min_), sec_
|
return int(deg_), int(min_), sec_
|
||||||
|
|
||||||
|
|
||||||
|
def _hardlink_file(src, dest):
|
||||||
|
""" Hardlinks a file from src path to dest path
|
||||||
|
src: source path as string
|
||||||
|
dest: destination path as string
|
||||||
|
Raises exception if linking fails or either path is None """
|
||||||
|
|
||||||
|
if src is None or dest is None:
|
||||||
|
raise ValueError("src and dest must not be None", src, dest)
|
||||||
|
|
||||||
|
if not os.path.isfile(src):
|
||||||
|
raise FileNotFoundError("src file does not appear to exist", src)
|
||||||
|
|
||||||
|
|
||||||
|
# if error on copy, subprocess will raise CalledProcessError
|
||||||
|
try:
|
||||||
|
os.link(src, dest)
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical(
|
||||||
|
f"ln returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _copy_file(src, dest, norsrc=False):
|
def _copy_file(src, dest, norsrc=False):
|
||||||
""" Copies a file from src path to dest path
|
""" Copies a file from src path to dest path
|
||||||
src: source path as string
|
src: source path as string
|
||||||
|
|||||||
@@ -241,6 +241,23 @@ def test_export():
|
|||||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_using_hardlinks():
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import export
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "--export-as-hardlink","-V"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
def test_export_current_name():
|
def test_export_current_name():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
@@ -473,11 +490,14 @@ def test_export_raw_edited_original():
|
|||||||
def test_export_directory_template_1():
|
def test_export_directory_template_1():
|
||||||
# test export using directory template
|
# test export using directory template
|
||||||
import glob
|
import glob
|
||||||
|
import locale
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.__main__ import export
|
from osxphotos.__main__ import export
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
|
|||||||
Reference in New Issue
Block a user