Added --cleanup, issue #262

This commit is contained in:
Rhet Turnbull
2020-12-05 21:22:49 -08:00
parent d371e63022
commit e5d6f21d8e
6 changed files with 386 additions and 213 deletions

131
README.md
View File

@@ -119,12 +119,12 @@ Example: `osxphotos help export`
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
Export photos from the Photos database. Export path DEST is required.
Optionally, query the Photos database using 1 or more search options; if
more than one option is provided, they are treated as "AND" (e.g. search
Optionally, query the Photos database using 1 or more search options; if
more than one option is provided, they are treated as "AND" (e.g. search
for photos matching all options). If no query options are provided, all
photos will be exported. By default, all versions of all photos will be
exported including edited versions, live photo movies, burst photos, and
associated raw images. See --skip-edited, --skip-live, --skip-bursts, and
associated raw images. See --skip-edited, --skip-live, --skip-bursts, and
--skip-raw options to modify this behavior.
Options:
@@ -265,6 +265,79 @@ Options:
photos if the raw photo does not have an
associated jpeg image (e.g. the raw file was
imported to Photos without a jpeg preview).
--current-name Use photo's current filename instead of
original filename for export. Note:
Starting with Photos 5, all photos are
renamed upon import. By default, photos are
exported with the the original name they had
before import.
--convert-to-jpeg Convert all non-jpeg images (e.g. raw, HEIC,
PNG, etc) to JPEG upon export. Only works
if your Mac has a GPU.
--jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
--convert-to-jpeg. A value of 1.0 specifies
best quality, a value of 0.0 specifies
maximum compression. Defaults to 1.0.
--download-missing Attempt to download missing photos from
iCloud. The current implementation uses
Applescript to interact with Photos to
export the photo which will force Photos to
download from iCloud if the photo does not
exist on disk. This will be slow and will
require internet connection. This obviously
only works if the Photos library is synched
to iCloud. Note: --download-missing does
not currently export all burst images; only
the primary photo will be exported--
associated burst images will be skipped.
--sidecar FORMAT Create sidecar for each photo exported;
valid FORMAT values: xmp, json; --sidecar
json: create JSON sidecar useable by
exiftool (https://exiftool.org/) The sidecar
file can be used to apply metadata to the
file with exiftool, for example: "exiftool
-j=photoname.jpg.json photoname.jpg" The
sidecar file is named in format
photoname.ext.json --sidecar xmp: create
XMP sidecar used by Adobe Lightroom, etc.The
sidecar file is named in format
photoname.ext.xmpThe XMP sidecar exports the
following tags: Description, Title,
Keywords/Tags, Subject (set to Keywords +
PersonInImage), PersonInImage, CreateDate,
ModifyDate, GPSLongitude. For a list of tags
exported in the JSON sidecar, see
--exiftool.
--exiftool Use exiftool to write metadata directly to
exported photos. To use this option,
exiftool must be installed and in the path.
exiftool may be installed from
https://exiftool.org/. Cannot be used with
--export-as-hardlink. Writes the following
metadata: EXIF:ImageDescription,
XMP:Description (see also --description-
template); XMP:Title; XMP:TagsList,
IPTC:Keywords (see also --keyword-template,
--person-keyword, --album-keyword);
XMP:Subject (set to keywords + person in
image to mirror Photos' behavior);
XMP:PersonInImage; EXIF:GPSLatitudeRef;
EXIF:GPSLongitudeRef; EXIF:GPSLatitude;
EXIF:GPSLongitude; EXIF:GPSPosition;
EXIF:DateTimeOriginal;
EXIF:OffsetTimeOriginal; EXIF:ModifyDate
(see --ignore-date-modified);
IPTC:DateCreated; IPTC:TimeCreated; (video
files only): QuickTime:CreationDate (UTC);
QuickTime:ModifyDate (UTC) (see also
--ignore-date-modified);
QuickTime:GPSCoordinates;
UserData:GPSCoordinates.
--ignore-date-modified If used with --exiftool or --sidecar, will
ignore the photo modification date and set
EXIF:ModifyDate to EXIF:DateTimeOriginal;
this is consistent with how Photos handles
the EXIF:ModifyDate tag.
--person-keyword Use person in image as keyword/tag when
exporting metadata.
--album-keyword Use album name as keyword/tag when exporting
@@ -291,53 +364,6 @@ Options:
could specify --description-template
"{descr} exported with osxphotos on
{today.date}" See Templating System below.
--current-name Use photo's current filename instead of
original filename for export. Note:
Starting with Photos 5, all photos are
renamed upon import. By default, photos are
exported with the the original name they had
before import.
--convert-to-jpeg Convert all non-jpeg images (e.g. raw, HEIC,
PNG, etc) to JPEG upon export. Only works
if your Mac has a GPU.
--jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
--convert-to-jpeg. A value of 1.0 specifies
best quality, a value of 0.0 specifies
maximum compression. Defaults to 1.0.
--sidecar FORMAT Create sidecar for each photo exported;
valid FORMAT values: xmp, json; --sidecar
json: create JSON sidecar useable by
exiftool (https://exiftool.org/) The sidecar
file can be used to apply metadata to the
file with exiftool, for example: "exiftool
-j=photoname.json photoname.jpg" The sidecar
file is named in format photoname.json
--sidecar xmp: create XMP sidecar used by
Adobe Lightroom, etc.The sidecar file is
named in format photoname.xmp
--download-missing Attempt to download missing photos from
iCloud. The current implementation uses
Applescript to interact with Photos to
export the photo which will force Photos to
download from iCloud if the photo does not
exist on disk. This will be slow and will
require internet connection. This obviously
only works if the Photos library is synched
to iCloud. Note: --download-missing does
not currently export all burst images; only
the primary photo will be exported--
associated burst images will be skipped.
--exiftool Use exiftool to write metadata directly to
exported photos. To use this option,
exiftool must be installed and in the path.
exiftool may be installed from
https://exiftool.org/. Cannot be used with
--export-as-hardlink.
--ignore-date-modified If used with --exiftool or --sidecar, will
ignore the photo modification date and set
EXIF:ModifyDate to EXIF:DateTimeOriginal;
this is consistent with how Photos handles
the EXIF:ModifyDate tag.
--directory DIRECTORY Optional template for specifying name of
output directory in the form
'{name,DEFAULT}'. See below for additional
@@ -379,6 +405,11 @@ Options:
default AppleScript interface.
--report REPORTNAME.CSV Write a CSV formatted report of all files
that were exported.
--cleanup Cleanup export directory by deleting any
files which were not included in this export
set. For example, photos which had
previously been exported and were
subsequently deleted in Photos.
-h, --help Show this message and exit.
** Export **

View File

@@ -1436,6 +1436,12 @@ def query(
help="Write a CSV formatted report of all files that were exported.",
type=click.Path(),
)
@click.option(
"--cleanup",
is_flag=True,
help="Cleanup export directory by deleting any files which were not included in this export set. "
"For example, photos which had previously been exported and were subsequently deleted in Photos.",
)
@DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True))
@click.pass_obj
@@ -1530,6 +1536,7 @@ def export(
use_photos_export,
use_photokit,
report,
cleanup,
):
"""Export photos from the Photos database.
Export path DEST is required.
@@ -1550,6 +1557,8 @@ def export(
click.echo(f"DEST {dest} must be valid path", err=True)
raise click.Abort()
dest = str(pathlib.Path(dest).resolve())
if report and os.path.isdir(report):
click.echo(f"report is a directory, must be file name", err=True)
raise click.Abort()
@@ -1579,6 +1588,7 @@ def export(
(shared, not_shared),
(has_comment, no_comment),
(has_likes, no_likes),
(export_as_hardlink, cleanup),
]
if any(all(bb) for bb in exclusive):
click.echo("Incompatible export options", err=True)
@@ -1890,7 +1900,6 @@ def export(
results_missing.extend(results.missing)
results_error.extend(results.error)
stop_time = time.perf_counter()
# print summary results
# print(f"results_exported: {results_exported}")
# print(f"results_new: {results_new}")
@@ -1899,11 +1908,36 @@ def export(
# print(f"results_exif_updated: {results_exif_updated}")
# print(f"results_touched: {results_touched}")
# print(f"results_converted: {results_converted}")
# print(f"results_sidecar_json: {results_sidecar_json}")
# print(f"results_sidecar_xmp: {results_sidecar_xmp}")
# print(f"results_sidecar_json_written: {results_sidecar_json_written}")
# print(f"results_sidecar_json_skipped: {results_sidecar_json_skipped}")
# print(f"results_sidecar_xmp_written: {results_sidecar_xmp_written}")
# print(f"results_sidecar_xmp_skipped: {results_sidecar_xmp_skipped}")
# print(f"results_missing: {results_missing}")
# print(f"results_error: {results_error}")
if cleanup:
all_files = (
results_exported
+ results_skipped
+ results_exif_updated
+ results_touched
+ results_converted
+ results_sidecar_json_written
+ results_sidecar_json_skipped
+ results_sidecar_xmp_written
+ results_sidecar_xmp_skipped
# include missing so a file that was already in export directory
# but was missing on --update doesn't get deleted
# (better to have old version than none)
+ results_missing
+ [str(pathlib.Path(export_db_path).resolve())]
)
click.echo(f"Cleaning up {dest}")
(cleaned_files, cleaned_dirs) = cleanup_files(dest, all_files, fileutil)
file_str = "files" if cleaned_files != 1 else "file"
dir_str = "directories" if cleaned_dirs != 1 else "directory"
click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}")
if report:
verbose(f"Writing export report to {report}")
write_export_report(
@@ -1942,6 +1976,7 @@ def export(
if touch_file:
summary += f", touched date: {len(results_touched)}"
click.echo(summary)
stop_time = time.perf_counter()
click.echo(f"Elapsed time: {(stop_time-start_time):.3f} seconds")
else:
click.echo("Did not find any photos to export")
@@ -2465,68 +2500,6 @@ def export_photo(
global VERBOSE
VERBOSE = bool(verbose_)
# TODO: if --skip-original-if-edited, it's possible edited version is on disk but
# original is missing, in which case we should download the edited version
if not download_missing:
if photo.ismissing:
space = " " if not verbose_ else ""
verbose(f"{space}Skipping missing photo {photo.original_filename}")
return ExportResults(
exported=[],
new=[],
updated=[],
skipped=[],
exif_updated=[],
touched=[],
converted_to_jpeg=[],
sidecar_json_written=[],
sidecar_json_skipped=[],
sidecar_xmp_written=[],
sidecar_xmp_skipped=[],
missing=[f"{photo.original_filename} ({photo.uuid})"],
error=[],
)
elif photo.path is None:
space = " " if not verbose_ else ""
verbose(
f"{space}WARNING: photo {photo.original_filename} ({photo.uuid}) is missing but ismissing=False, "
f"skipping {photo.original_filename}"
)
return ExportResults(
exported=[],
new=[],
updated=[],
skipped=[],
exif_updated=[],
touched=[],
converted_to_jpeg=[],
sidecar_json_written=[],
sidecar_json_skipped=[],
sidecar_xmp_written=[],
sidecar_xmp_skipped=[],
missing=[f"{photo.original_filename} ({photo.uuid})"],
error=[],
)
elif photo.ismissing and not photo.iscloudasset and not photo.incloud:
verbose(
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
)
return ExportResults(
exported=[],
new=[],
updated=[],
skipped=[],
exif_updated=[],
touched=[],
converted_to_jpeg=[],
sidecar_json_written=[],
sidecar_json_skipped=[],
sidecar_xmp_written=[],
sidecar_xmp_skipped=[],
missing=[f"{photo.original_filename} ({photo.uuid})"],
error=[],
)
results_exported = []
results_new = []
results_updated = []
@@ -2539,6 +2512,7 @@ def export_photo(
results_sidecar_xmp_written = []
results_sidecar_xmp_skipped = []
results_error = []
results_missing = []
export_original = not (skip_original_if_edited and photo.hasadjustments)
@@ -2558,6 +2532,29 @@ def export_photo(
f"Edited file for {photo.original_filename} is missing, exporting original"
)
# check for missing photos before downloading
missing_original = False
missing_edited = False
if download_missing:
if (
(photo.ismissing or photo.path is None)
and not photo.iscloudasset
and not photo.incloud
):
missing_original = True
if (
photo.hasadjustments
and photo.path_edited is None
and not photo.iscloudasset
and not photo.incloud
):
missing_edited = True
else:
if photo.ismissing or photo.path is None:
missing_original = True
if photo.hasadjustments and photo.path_edited is None:
missing_edited = True
filenames = get_filenames_from_template(photo, filename_template, original_name)
for filename in filenames:
if original_suffix:
@@ -2598,73 +2595,73 @@ def export_photo(
# export the photo to each path in dest_paths
for dest_path in dest_paths:
# TODO: if --skip-original-if-edited, it's possible edited version is on disk but
# original is missing, in which case we should download the edited version
if export_original:
try:
export_results = photo.export2(
dest_path,
original_filename,
sidecar_json=sidecar_json,
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,
no_xattr=no_extended_attributes,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose,
if missing_original:
space = " " if not verbose_ else ""
verbose(f"{space}Skipping missing photo {photo.original_filename}")
results_missing.append(
str(pathlib.Path(dest_path) / original_filename)
)
else:
try:
export_results = photo.export2(
dest_path,
original_filename,
sidecar_json=sidecar_json,
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,
no_xattr=no_extended_attributes,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose,
)
results_exported.extend(export_results.exported)
results_new.extend(export_results.new)
results_updated.extend(export_results.updated)
results_skipped.extend(export_results.skipped)
results_exif_updated.extend(export_results.exif_updated)
results_touched.extend(export_results.touched)
results_converted.extend(export_results.converted_to_jpeg)
results_sidecar_json_written.extend(
export_results.sidecar_json_written
)
results_sidecar_json_skipped.extend(
export_results.sidecar_json_skipped
)
results_sidecar_xmp_written.extend(
export_results.sidecar_xmp_written
)
results_sidecar_xmp_skipped.extend(
export_results.sidecar_xmp_skipped
)
results_exported.extend(export_results.exported)
results_new.extend(export_results.new)
results_updated.extend(export_results.updated)
results_skipped.extend(export_results.skipped)
results_exif_updated.extend(export_results.exif_updated)
results_touched.extend(export_results.touched)
results_converted.extend(export_results.converted_to_jpeg)
results_sidecar_json_written.extend(
export_results.sidecar_json_written
)
results_sidecar_json_skipped.extend(
export_results.sidecar_json_skipped
)
results_sidecar_xmp_written.extend(
export_results.sidecar_xmp_written
)
results_sidecar_xmp_skipped.extend(
export_results.sidecar_xmp_skipped
)
if verbose_:
for exported in export_results.exported:
verbose(f"Exported {exported}")
for new in export_results.new:
verbose(f"Exported new file {new}")
for updated in export_results.updated:
verbose(f"Exported updated file {updated}")
for skipped in export_results.skipped:
verbose(f"Skipped up to date file {skipped}")
for touched in export_results.touched:
verbose(f"Touched date on file {touched}")
except Exception:
click.echo(
f"Error exporting photo {photo.original_filename} ({photo.filename}) as {original_filename}",
err=True,
)
results_error.extend(dest)
except Exception:
click.echo(
f"Error exporting photo {photo.original_filename} ({photo.filename}) as {original_filename}",
err=True,
)
results_error.extend(
str(pathlib.Path(dest) / original_filename)
)
else:
verbose(f"Skipping original version of {photo.original_filename}")
@@ -2673,23 +2670,26 @@ def export_photo(
if export_edited and photo.hasadjustments:
# if download_missing and the photo is missing or path doesn't exist,
# try to download with Photos
if not download_missing and photo.path_edited is None:
verbose(f"Skipping missing edited photo for {filename}")
edited_filename = pathlib.Path(filename)
# check for correct edited suffix
if photo.path_edited is not None:
edited_ext = pathlib.Path(photo.path_edited).suffix
else:
edited_filename = pathlib.Path(filename)
# check for correct edited suffix
if photo.path_edited is not None:
edited_ext = pathlib.Path(photo.path_edited).suffix
else:
# use filename suffix which might be wrong,
# will be corrected by use_photos_export
edited_ext = pathlib.Path(photo.filename).suffix
edited_filename = (
f"{edited_filename.stem}{edited_suffix}{edited_ext}"
)
verbose(
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
# use filename suffix which might be wrong,
# will be corrected by use_photos_export
edited_ext = pathlib.Path(photo.filename).suffix
edited_filename = f"{edited_filename.stem}{edited_suffix}{edited_ext}"
verbose(
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
)
if missing_edited:
space = " " if not verbose_ else ""
verbose(f"{space}Skipping missing edited photo for {filename}")
results_missing.append(
str(pathlib.Path(dest_path) / edited_filename)
)
else:
try:
export_results_edited = photo.export2(
dest_path,
@@ -2740,23 +2740,26 @@ def export_photo(
export_results_edited.sidecar_xmp_skipped
)
if verbose_:
for exported in export_results_edited.exported:
verbose(f"Exported {exported}")
for new in export_results_edited.new:
verbose(f"Exported new file {new}")
for updated in export_results_edited.updated:
verbose(f"Exported updated file {updated}")
for skipped in export_results_edited.skipped:
verbose(f"Skipped up to date file {skipped}")
for touched in export_results_edited.touched:
verbose(f"Touched date on file {touched}")
except Exception:
click.echo(
f"Error exporting photo {filename} as {edited_filename}",
err=True,
)
results_error.extend(dest)
results_error.extend(str(pathlib.Path(dest) / edited_filename))
if verbose_:
if update:
for new in results_new:
verbose(f"Exported new file {new}")
for updated in results_updated:
verbose(f"Exported updated file {updated}")
for skipped in results_skipped:
verbose(f"Skipped up to date file {skipped}")
else:
for exported in results_exported:
verbose(f"Exported {exported}")
for touched in results_touched:
verbose(f"Touched date on file {touched}")
return ExportResults(
exported=results_exported,
@@ -2770,7 +2773,7 @@ def export_photo(
sidecar_json_skipped=results_sidecar_json_skipped,
sidecar_xmp_written=results_sidecar_xmp_written,
sidecar_xmp_skipped=results_sidecar_xmp_skipped,
missing=[],
missing=results_missing,
error=results_error,
)
@@ -3043,5 +3046,40 @@ def write_export_report(
raise click.Abort()
def cleanup_files(dest_path, files_to_keep, fileutil):
""" cleanup dest_path by deleting and files and empty directories
not in files_to_keep
Args:
dest_path: path to directory to clean
files_to_keep: list of full file paths to keep (not delete)
fileutile: FileUtil object
Returns:
tuple of (number of files deleted, number of directories deleted)
"""
keepers = {filename.lower(): 1 for filename in files_to_keep}
deleted_files = 0
for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower()
if p.is_file() and path not in keepers:
verbose(f"Deleting {p}")
fileutil.unlink(p)
deleted_files += 1
# delete empty directories
deleted_dirs = 0
for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower()
# if directory and directory is empty
if p.is_dir() and not next(p.iterdir(), False):
verbose(f"Deleting empty directory {p}")
fileutil.rmdir(p)
deleted_dirs += 1
return (deleted_files, deleted_dirs)
if __name__ == "__main__":
cli() # pylint: disable=no-value-for-parameter

View File

@@ -1,4 +1,4 @@
""" version info """
__version__ = "0.37.5"
__version__ = "0.37.6"

View File

@@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
from .imageconverter import ImageConverter
class FileUtilABC(ABC):
""" Abstract base class for FileUtil """
@@ -27,6 +28,11 @@ class FileUtilABC(ABC):
def unlink(cls, dest):
pass
@classmethod
@abstractmethod
def rmdir(cls, dest):
pass
@classmethod
@abstractmethod
def utime(cls, path, times):
@@ -114,6 +120,14 @@ class FileUtilMacOS(FileUtilABC):
else:
os.unlink(filepath)
@classmethod
def rmdir(cls, dirpath):
""" remove directory filepath; dirpath must be empty """
if isinstance(dirpath, pathlib.Path):
dirpath.rmdir()
else:
os.rmdir(dirpath)
@classmethod
def utime(cls, path, times):
""" Set the access and modified time of path. """
@@ -164,7 +178,7 @@ class FileUtilMacOS(FileUtilABC):
def file_sig(cls, f1):
""" return os.stat signature for file f1 """
return cls._sig(os.stat(f1))
@classmethod
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
""" converts image file src_file to jpeg format as dest_file
@@ -178,7 +192,9 @@ class FileUtilMacOS(FileUtilABC):
True if success, otherwise False
"""
converter = ImageConverter()
return converter.write_jpeg(src_file, dest_file, compression_quality=compression_quality)
return converter.write_jpeg(
src_file, dest_file, compression_quality=compression_quality
)
@staticmethod
def _sig(st):
@@ -189,6 +205,7 @@ class FileUtilMacOS(FileUtilABC):
# use int(st.st_mtime) because ditto does not copy fractional portion of mtime
return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime))
class FileUtil(FileUtilMacOS):
""" Various file utilities """
@@ -228,6 +245,10 @@ class FileUtilNoOp(FileUtil):
def unlink(cls, dest):
cls.verbose(f"unlink: {dest}")
@classmethod
def rmdir(cls, dest):
cls.verbose(f"rmdir: {dest}")
@classmethod
def utime(cls, path, times):
cls.verbose(f"utime: {path}, {times}")

View File

@@ -3272,9 +3272,9 @@ def test_export_then_hardlink():
def test_export_dry_run():
""" test export with dry-run flag """
import glob
import os
import os.path
import re
import osxphotos
from osxphotos.__main__ import export
@@ -3288,7 +3288,7 @@ def test_export_dry_run():
assert result.exit_code == 0
assert "Processed: 7 photos, exported: 8, missing: 1, error: 0" in result.output
for filepath in CLI_EXPORT_FILENAMES:
assert f"Exported {filepath}" in result.output
assert re.search(r"Exported.*" + f"{filepath}", result.output)
assert not os.path.isfile(filepath)
@@ -3340,10 +3340,10 @@ def test_export_update_edits_dry_run():
def test_export_directory_template_1_dry_run():
""" test export using directory template with dry-run flag """
import glob
import locale
import os
import os.path
import re
import osxphotos
from osxphotos.__main__ import export
@@ -3368,7 +3368,7 @@ def test_export_directory_template_1_dry_run():
assert "exported: 8" in result.output
workdir = os.getcwd()
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1:
assert f"Exported {filepath}" in result.output
assert re.search(r"Exported.*" + f"{filepath}", result.output)
assert not os.path.isfile(os.path.join(workdir, filepath))
@@ -3914,3 +3914,73 @@ def test_export_missing_not_download_missing():
)
assert result.exit_code != 0
assert "Aborted!" in result.output
def test_export_cleanup():
""" test export with --cleanup flag """
import pathlib
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), ".", "-V"])
assert result.exit_code == 0
# create 2 files and a directory
with open("delete_me.txt", "w") as fd:
fd.write("delete me!")
os.mkdir("./foo")
with open("foo/delete_me_too.txt", "w") as fd:
fd.write("delete me too!")
assert pathlib.Path("./delete_me.txt").is_file()
# run cleanup with dry-run
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--update",
"--cleanup",
"--dry-run",
],
)
assert "Deleted: 2 files, 0 directories" in result.output
assert pathlib.Path("./delete_me.txt").is_file()
assert pathlib.Path("./foo/delete_me_too.txt").is_file()
# run cleanup without dry-run
result = runner.invoke(
export,
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--update", "--cleanup"],
)
assert "Deleted: 2 files, 1 directory" in result.output
assert not pathlib.Path("./delete_me.txt").is_file()
assert not pathlib.Path("./foo/delete_me_too.txt").is_file()
def test_export_cleanup_export_as_hardling():
""" test export with incompatible option """
import os
import os.path
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),
".",
"-V",
"--export-as-hardlink",
"--cleanup",
],
)
assert "Incompatible export options" in result.output

View File

@@ -73,6 +73,18 @@ def test_unlink_file():
assert not os.path.isfile(dest)
def test_rmdir():
import os.path
import tempfile
from osxphotos.fileutil import FileUtil
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dir_name = temp_dir.name
assert os.path.isdir(dir_name)
FileUtil.rmdir(dir_name)
assert not os.path.isdir(dir_name)
@pytest.mark.skipif(
"OSXPHOTOS_TEST_CONVERT" not in os.environ,
reason="Skip if running in Github actions, no GPU.",
@@ -90,6 +102,7 @@ def test_convert_to_jpeg():
assert FileUtil.convert_to_jpeg(imgfile, outfile)
assert outfile.is_file()
@pytest.mark.skipif(
"OSXPHOTOS_TEST_CONVERT" not in os.environ,
reason="Skip if running in Github actions, no GPU.",