added --export-as-hardlink option
This commit is contained in:
@@ -89,7 +89,7 @@ class ExportCommand(click.Command):
|
||||
)
|
||||
formatter.write("\n")
|
||||
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), "
|
||||
+ "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}"'
|
||||
@@ -860,6 +860,11 @@ def query(
|
||||
@DB_OPTION
|
||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||
@query_options
|
||||
@click.option(
|
||||
"--export-as-hardlink",
|
||||
is_flag=True,
|
||||
help="Hardlink files instead of copying them. ",
|
||||
)
|
||||
@click.option(
|
||||
"--overwrite",
|
||||
is_flag=True,
|
||||
@@ -1004,6 +1009,7 @@ def export(
|
||||
from_date,
|
||||
to_date,
|
||||
verbose,
|
||||
export_as_hardlink,
|
||||
overwrite,
|
||||
export_by_date,
|
||||
skip_edited,
|
||||
@@ -1195,6 +1201,7 @@ def export(
|
||||
verbose,
|
||||
export_by_date,
|
||||
sidecar,
|
||||
export_as_hardlink,
|
||||
overwrite,
|
||||
export_edited,
|
||||
original_name,
|
||||
@@ -1216,6 +1223,7 @@ def export(
|
||||
verbose,
|
||||
export_by_date,
|
||||
sidecar,
|
||||
export_as_hardlink,
|
||||
overwrite,
|
||||
export_edited,
|
||||
original_name,
|
||||
@@ -1605,6 +1613,7 @@ def export_photo(
|
||||
verbose,
|
||||
export_by_date,
|
||||
sidecar,
|
||||
export_as_hardlink,
|
||||
overwrite,
|
||||
export_edited,
|
||||
original_name,
|
||||
@@ -1624,6 +1633,7 @@ def export_photo(
|
||||
verbose: boolean; print verbose output
|
||||
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
|
||||
export_as_hardlink: boolean; hardlink files instead of copying them
|
||||
overwrite: boolean; overwrite dest file if it already exists
|
||||
original_name: boolean; use original filename instead of current filename
|
||||
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,
|
||||
live_photo=export_live,
|
||||
raw_photo=export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
@@ -1752,6 +1763,7 @@ def export_photo(
|
||||
edited_name,
|
||||
sidecar_json=sidecar_json,
|
||||
sidecar_xmp=sidecar_xmp,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
edited=True,
|
||||
use_photos_export=use_photos_export,
|
||||
|
||||
@@ -40,6 +40,7 @@ from .template import (
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||
)
|
||||
from .utils import (
|
||||
_hardlink_file,
|
||||
_copy_file,
|
||||
_export_photo_uuid_applescript,
|
||||
_get_resource_loc,
|
||||
@@ -635,6 +636,7 @@ class PhotoInfo:
|
||||
edited=False,
|
||||
live_photo=False,
|
||||
raw_photo=False,
|
||||
export_as_hardlink=False,
|
||||
overwrite=False,
|
||||
increment=True,
|
||||
sidecar_json=False,
|
||||
@@ -660,6 +662,7 @@ class PhotoInfo:
|
||||
(or raise exception if no edited version)
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -802,7 +805,10 @@ class PhotoInfo:
|
||||
)
|
||||
|
||||
# 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))
|
||||
|
||||
# copy live photo associated .mov if requested
|
||||
@@ -814,7 +820,10 @@ class PhotoInfo:
|
||||
logging.debug(
|
||||
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))
|
||||
else:
|
||||
logging.warning(f"Skipping missing live movie for {filename}")
|
||||
@@ -828,7 +837,10 @@ class PhotoInfo:
|
||||
logging.debug(
|
||||
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))
|
||||
else:
|
||||
logging.warning(f"Skipping missing RAW photo for {filename}")
|
||||
|
||||
@@ -118,6 +118,32 @@ def _dd_to_dms(dd):
|
||||
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)
|
||||
|
||||
command = ["ln", src, dest]
|
||||
|
||||
# if error on copy, subprocess will raise CalledProcessError
|
||||
try:
|
||||
result = subprocess.run(command, check=True, stderr=subprocess.PIPE)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.critical(
|
||||
f"ln returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}"
|
||||
)
|
||||
raise e
|
||||
|
||||
return result.returncode
|
||||
|
||||
|
||||
def _copy_file(src, dest, norsrc=False):
|
||||
""" Copies a file from src path to dest path
|
||||
src: source path as string
|
||||
|
||||
Reference in New Issue
Block a user