Added --no-extended-attributes option to CLI, closes #85

This commit is contained in:
Rhet Turnbull
2020-04-05 09:13:52 -07:00
parent 6073acc9d3
commit ddaa66d19e
7 changed files with 96 additions and 10 deletions

View File

@@ -892,7 +892,7 @@ Returns True if photo is a panorama, otherwise False.
#### `json()`
Returns a JSON representation of all photo info
#### `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)`
#### `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)`
Export photo from the Photos library to another destination on disk.
- dest: must be valid destination path as str (or exception raised).
@@ -906,6 +906,7 @@ Export photo from the Photos library to another destination on disk.
- use_photos_export: boolean; (default=False), if True will attempt to export photo via applescript interaction with Photos; useful for forcing download of missing photos. This only works if the Photos library being used is the default library (last opened by Photos) as applescript will directly interact with whichever library Photos is currently using.
- timeout: (int, default=120) timeout in seconds used with use_photos_export
- exiftool: (boolean, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
- no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes
Returns: list of paths to exported files. More than one file could be exported, for example if live_photo=True, both the original imaage and the associated .mov file will be exported

View File

@@ -912,6 +912,14 @@ def query(
help="Optional template for specifying name of output directory in the form '{name,DEFAULT}'. "
"See below for additional details on templating system.",
)
@click.option(
"--no-extended-attributes",
is_flag=True,
default=False,
help="Don't copy extended attributes when exporting. You only need this if exporting "
"to a filesystem that doesn't support Mac OS extended attributes. Only use this if you get "
"an error while exporting.",
)
@DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True))
@click.pass_obj
@@ -975,6 +983,7 @@ def export(
directory,
place,
no_place,
no_extended_attributes,
):
""" Export photos from the Photos database.
Export path DEST is required.
@@ -1117,6 +1126,7 @@ def export(
download_missing,
exiftool,
directory,
no_extended_attributes,
)
else:
for p in photos:
@@ -1133,6 +1143,7 @@ def export(
download_missing,
exiftool,
directory,
no_extended_attributes,
)
if export_paths:
click.echo(f"Exported {p.filename} to {export_paths}")
@@ -1491,6 +1502,7 @@ def export_photo(
download_missing,
exiftool,
directory,
no_extended_attributes,
):
""" Helper function for export that does the actual export
photo: PhotoInfo object
@@ -1505,6 +1517,7 @@ def export_photo(
download_missing: attempt download of missing iCloud photos
exiftool: use exiftool to write EXIF metadata directly to exported photo
directory: template used to determine output directory
no_extended_attributes: boolean; if True, exports photo without preserving extended attributes
returns list of path(s) of exported photo or None if photo was missing
"""
@@ -1586,6 +1599,7 @@ def export_photo(
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
no_xattr=no_extended_attributes,
)[0]
photo_paths.append(photo_path)
@@ -1620,6 +1634,7 @@ def export_photo(
edited=True,
use_photos_export=use_photos_export,
exiftool=exiftool,
no_xattr=no_extended_attributes,
)
return photo_paths

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.25.0"
__version__ = "0.25.1"

View File

@@ -522,6 +522,7 @@ class PhotoInfo:
use_photos_export=False,
timeout=120,
exiftool=False,
no_xattr=False,
):
""" export photo
dest: must be valid destination path (or exception raised)
@@ -545,6 +546,7 @@ class PhotoInfo:
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes
returns list of full paths to the exported files """
# list of all files exported during this call to export
@@ -667,7 +669,7 @@ class PhotoInfo:
)
# copy the file, _copy_file uses ditto to preserve Mac extended attributes
_copy_file(src, dest)
_copy_file(src, dest, norsrc=no_xattr)
exported_files.append(str(dest))
# copy live photo associated .mov if requested
@@ -679,7 +681,7 @@ class PhotoInfo:
logging.debug(
f"Exporting live photo video of {filename} as {live_name.name}"
)
_copy_file(src_live, str(live_name))
_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}")

View File

@@ -114,10 +114,14 @@ def _dd_to_dms(dd):
return int(deg_), int(min_), sec_
def _copy_file(src, dest):
def _copy_file(src, dest, norsrc=False):
""" Copies a file from src path to dest path
src: source path as string
dest: destination path as string
norsrc: (bool) if True, uses --norsrc flag with ditto so it will not copy
resource fork or extended attributes. May be useful on volumes that
don't work with extended attributes (likely only certain SMB mounts)
default is False
Uses ditto to perform copy; will silently overwrite dest if it exists
Raises exception if copy fails or either path is None """
@@ -125,19 +129,24 @@ def _copy_file(src, dest):
raise ValueError("src and dest must not be None", src, dest)
if not os.path.isfile(src):
raise ValueError("src file does not appear to exist", src)
raise FileNotFoundError("src file does not appear to exist", src)
if norsrc:
command = ["/usr/bin/ditto", "--norsrc", src, dest]
else:
command = ["/usr/bin/ditto", src, dest]
# if error on copy, subprocess will raise CalledProcessError
try:
subprocess.run(
["/usr/bin/ditto", src, dest], check=True, stderr=subprocess.PIPE
)
result = subprocess.run(command, check=True, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
logging.critical(
f"ditto returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}"
)
raise e
return result.returncode
def dd_to_dms_str(lat, lon):
""" convert latitude, longitude in degrees to degrees, minutes, seconds as string """

View File

@@ -374,6 +374,28 @@ def test_export_13():
assert e.type == type(FileNotFoundError())
def test_export_no_xattr():
# test basic export with no_xattr=True
# get an unedited image and export it using default filename
import os
import os.path
import tempfile
import osxphotos
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dest = tempdir.name
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, no_xattr=True)[0]
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
def test_dd_to_dms_str_1():
import osxphotos
@@ -517,4 +539,3 @@ def test_xmp_sidecar():
for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
assert line_expected == line_got

View File

@@ -51,3 +51,41 @@ def test_db_is_locked_unlocked():
import osxphotos
assert not osxphotos.utils._db_is_locked(DB_UNLOCKED_10_15)
def test_copy_file_valid():
# _copy_file with valid src, dest
import os.path
import tempfile
from osxphotos.utils import _copy_file
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
src = "tests/test-images/wedding.jpg"
result = _copy_file(src, temp_dir.name)
assert result == 0
assert os.path.isfile(os.path.join(temp_dir.name, "wedding.jpg"))
def test_copy_file_invalid():
# _copy_file with invalid src
import tempfile
from osxphotos.utils import _copy_file
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
src = "tests/test-images/wedding_DOES_NOT_EXIST.jpg"
with pytest.raises(Exception) as e:
assert _copy_file(src, temp_dir.name)
assert e.type == FileNotFoundError
def test_copy_file_norsrc():
# _copy_file with --norsrc
import os.path
import tempfile
from osxphotos.utils import _copy_file
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
src = "tests/test-images/wedding.jpg"
result = _copy_file(src, temp_dir.name, norsrc=True)
assert result == 0
assert os.path.isfile(os.path.join(temp_dir.name, "wedding.jpg"))