Add Clone command and fixes

This commit is contained in:
Cédric Leporcq 2021-12-05 18:27:04 +01:00
parent d55fc63a41
commit f0a7624b0f
7 changed files with 113 additions and 23 deletions

View File

@ -8,6 +8,7 @@ import click
from ordigi import constants, log, LOG from ordigi import constants, log, LOG
from ordigi.collection import Collection from ordigi.collection import Collection
from ordigi.geolocation import GeoLocation from ordigi.geolocation import GeoLocation
from ordigi import utils
_logger_options = [ _logger_options = [
click.option( click.option(
@ -173,7 +174,7 @@ def _check(**kwargs):
if summary.errors: if summary.errors:
sys.exit(1) sys.exit(1)
else: else:
LOG.logger.error('Db data is not accurate run `ordigi update`') LOG.error('Db data is not accurate run `ordigi update`')
sys.exit(1) sys.exit(1)
@ -256,6 +257,39 @@ def _clean(**kwargs):
sys.exit(1) sys.exit(1)
@cli.command('clone')
@add_options(_logger_options)
@add_options(_dry_run_options)
@click.argument('src', required=True, nargs=1, type=click.Path())
@click.argument('dest', required=True, nargs=1, type=click.Path())
def _clone(**kwargs):
"""Clone media collection to another location"""
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
src_path = Path(kwargs['src']).expanduser().absolute()
dest_path = Path(kwargs['dest']).expanduser().absolute()
dry_run = kwargs['dry_run']
src_collection = Collection(
src_path, {'cache': True, 'dry_run': dry_run}
)
if dest_path.exists() and not utils.empty_dir(dest_path):
LOG.error(f'Destination collection path {dest_path} must be empty directory')
sys.exit(1)
summary = src_collection.clone(dest_path)
if log_level < 30:
summary.print()
if summary.errors:
sys.exit(1)
@cli.command('compare') @cli.command('compare')
@add_options(_logger_options) @add_options(_logger_options)
@add_options(_dry_run_options) @add_options(_dry_run_options)

View File

@ -3,6 +3,7 @@ Collection methods.
""" """
from copy import copy from copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from distutils.dir_util import copy_tree
import filecmp import filecmp
from fnmatch import fnmatch from fnmatch import fnmatch
import os import os
@ -299,6 +300,12 @@ class FileIO:
self.log.info(f'remove: {path}') self.log.info(f'remove: {path}')
def mkdir(self, directory):
if not self.dry_run:
directory.mkdir(exist_ok=True)
self.log.info(f'create dir: {directory}')
def rmdir(self, directory): def rmdir(self, directory):
if not self.dry_run: if not self.dry_run:
directory.rmdir() directory.rmdir()
@ -699,14 +706,8 @@ class Collection(SortMedias):
if not cli_options: if not cli_options:
cli_options = {} cli_options = {}
self.log = LOG.getChild(self.__class__.__name__)
# Check if collection path is valid
if not root.exists():
self.log.error(f'Collection path {root} does not exist')
sys.exit(1)
self.root = root self.root = root
self.log = LOG.getChild(self.__class__.__name__)
# Get config options # Get config options
self.opt = self.get_config_options() self.opt = self.get_config_options()
@ -720,8 +721,11 @@ class Collection(SortMedias):
if not self.exclude: if not self.exclude:
self.exclude = set() self.exclude = set()
self.db = CollectionDb(root)
self.fileio = FileIO(self.opt['Terminal']['dry_run']) self.fileio = FileIO(self.opt['Terminal']['dry_run'])
self.root_is_valid()
self.db = CollectionDb(root)
self.paths = Paths( self.paths = Paths(
self.opt['Filters'], self.opt['Filters'],
interactive=self.opt['Terminal']['interactive'], interactive=self.opt['Terminal']['interactive'],
@ -749,6 +753,16 @@ class Collection(SortMedias):
self.summary = Summary(self.root) self.summary = Summary(self.root)
self.theme = request.load_theme() self.theme = request.load_theme()
def root_is_valid(self):
"""Check if collection path is valid"""
if self.root.exists():
if not self.root.is_dir():
self.log.error(f'Collection path {self.root} is not a directory')
sys.exit(1)
else:
self.log.error(f'Collection path {self.root} does not exist')
sys.exit(1)
def get_config_options(self): def get_config_options(self):
"""Get collection config""" """Get collection config"""
config = Config(self.root.joinpath('.ordigi', 'ordigi.conf')) config = Config(self.root.joinpath('.ordigi', 'ordigi.conf'))
@ -810,6 +824,14 @@ class Collection(SortMedias):
return True return True
def check(self):
if self.db.sqlite.is_empty('metadata'):
self.log.error('Db data does not exist run `ordigi init`')
sys.exit(1)
elif not self.check_db():
self.log.error('Db data is not accurate run `ordigi update`')
sys.exit(1)
def _init_check_db(self, loc=None): def _init_check_db(self, loc=None):
if self.db.sqlite.is_empty('metadata'): if self.db.sqlite.is_empty('metadata'):
self.init(loc) self.init(loc)
@ -817,6 +839,25 @@ class Collection(SortMedias):
self.log.error('Db data is not accurate run `ordigi update`') self.log.error('Db data is not accurate run `ordigi update`')
sys.exit(1) sys.exit(1)
def clone(self, dest_path):
"""Clone collection in another location"""
self.check()
if not self.dry_run:
copy_tree(str(self.root), str(dest_path))
self.log.info(f'copy: {self.root} -> {dest_path}')
if not self.dry_run:
dest_collection = Collection(
dest_path, {'cache': True, 'dry_run': self.dry_run}
)
if not dest_collection.check_db():
self.summary.append('check', False)
return self.summary
def update(self, loc): def update(self, loc):
"""Update collection db""" """Update collection db"""
file_paths = list(self.get_collection_files()) file_paths = list(self.get_collection_files())
@ -866,7 +907,7 @@ class Collection(SortMedias):
if checksum == self.db.sqlite.get_checksum(relpath): if checksum == self.db.sqlite.get_checksum(relpath):
self.summary.append('check', True, file_path) self.summary.append('check', True, file_path)
else: else:
self.log.error('{file_path} is corrupted') self.log.error(f'{file_path} is corrupted')
self.summary.append('check', False, file_path) self.summary.append('check', False, file_path)
return self.summary return self.summary

View File

@ -1,19 +1,13 @@
from datetime import datetime from datetime import datetime
import json
import os import os
from pathlib import Path from pathlib import Path
import sqlite3 import sqlite3
import sys import sys
from shutil import copyfile
from time import strftime
from ordigi import constants
from ordigi.utils import distance_between_two_points from ordigi.utils import distance_between_two_points
class Sqlite: class Sqlite:
"""Methods for interacting with Sqlite database""" """Methods for interacting with Sqlite database"""
def __init__(self, target_dir): def __init__(self, target_dir):
@ -30,7 +24,7 @@ class Sqlite:
self.db_type = 'SQLite format 3' self.db_type = 'SQLite format 3'
self.types = {'text': (str, datetime), 'integer': (int,), 'real': (float,)} self.types = {'text': (str, datetime), 'integer': (int,), 'real': (float,)}
self.filename = Path(db_dir, target_dir.name + '.db') self.filename = Path(db_dir, 'collection.db')
self.con = sqlite3.connect(self.filename) self.con = sqlite3.connect(self.filename)
# Allow selecting column by name # Allow selecting column by name
self.con.row_factory = sqlite3.Row self.con.row_factory = sqlite3.Row

View File

@ -1,6 +1,7 @@
# import pandas as pd # import pandas as pd
from tabulate import tabulate from tabulate import tabulate
class Tables: class Tables:
"""Create table and display result in Pandas DataFrame""" """Create table and display result in Pandas DataFrame"""
@ -34,6 +35,7 @@ class Tables:
errors_headers = self.columns errors_headers = self.columns
return tabulate(self.table, headers=errors_headers) return tabulate(self.table, headers=errors_headers)
class Summary: class Summary:
"""Result summary of ordigi program call""" """Result summary of ordigi program call"""
@ -61,7 +63,7 @@ class Summary:
self.errors_table.append(action, file_path, dest_path) self.errors_table.append(action, file_path, dest_path)
if not success: if not success:
self.errors +=1 self.errors += 1
def print(self): def print(self):
"""Print summary""" """Print summary"""

View File

@ -38,6 +38,10 @@ def distance_between_two_points(lat1, lon1, lat2, lon2):
return rad * sqrt(x * x + y * y) return rad * sqrt(x * x + y * y)
def empty_dir(dir_path):
return not next(os.scandir(dir_path), None)
def get_date_regex(user_regex=None): def get_date_regex(user_regex=None):
"""Return date regex generator""" """Return date regex generator"""
if user_regex: if user_regex:

View File

@ -2,18 +2,18 @@
from configparser import RawConfigParser from configparser import RawConfigParser
import os import os
import pytest
from pathlib import Path, PurePath from pathlib import Path, PurePath
import random import random
import shutil import shutil
import string
import tempfile import tempfile
from ordigi.config import Config import pytest
from ordigi.exiftool import _ExifToolProc from ordigi.exiftool import _ExifToolProc
ORDIGI_PATH = Path(__file__).parent.parent ORDIGI_PATH = Path(__file__).parent.parent
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def reset_singletons(): def reset_singletons():
""" Need to clean up any ExifTool singletons between tests """ """ Need to clean up any ExifTool singletons between tests """
@ -33,7 +33,6 @@ def sample_files_paths(tmpdir_factory):
def randomize_files(dest_dir): def randomize_files(dest_dir):
# Get files randomly # Get files randomly
paths = Path(dest_dir).glob('*')
for path, subdirs, files in os.walk(dest_dir): for path, subdirs, files in os.walk(dest_dir):
if '.ordigi' in path: if '.ordigi' in path:
continue continue
@ -50,7 +49,7 @@ def randomize_files(dest_dir):
def randomize_db(dest_dir): def randomize_db(dest_dir):
# alterate database # alterate database
file_path = Path(str(dest_dir), '.ordigi', str(dest_dir.name) + '.db') file_path = Path(str(dest_dir), '.ordigi', 'collection.db')
with open(file_path, 'wb') as fout: with open(file_path, 'wb') as fout:
fout.write(os.urandom(random.randrange(128, 2048))) fout.write(os.urandom(random.randrange(128, 2048)))

View File

@ -72,6 +72,8 @@ class TestOrdigi:
for command in commands: for command in commands:
self.assert_cli(command, ['not_exist'], state=1) self.assert_cli(command, ['not_exist'], state=1)
self.assert_cli(cli._clone, ['not_exist'], state=2)
def test_sort(self): def test_sort(self):
bool_options = ( bool_options = (
# '--interactive', # '--interactive',
@ -97,6 +99,20 @@ class TestOrdigi:
self.assert_options(cli._sort, bool_options, arg_options, paths) self.assert_options(cli._sort, bool_options, arg_options, paths)
self.assert_all_options(cli._sort, bool_options, arg_options, paths) self.assert_all_options(cli._sort, bool_options, arg_options, paths)
def test_clone(self, tmp_path):
arg_options = (
*self.logger_options,
)
paths = (str(self.src_path), str(tmp_path))
self.assert_cli(cli._init, [str(self.src_path)])
self.assert_cli(cli._clone, ['--dry-run', '--verbose', 'DEBUG', *paths])
self.assert_cli(cli._clone, paths)
def assert_init(self): def assert_init(self):
for opt, arg in self.logger_options: for opt, arg in self.logger_options:
self.assert_cli(cli._init, [opt, arg, str(self.src_path)]) self.assert_cli(cli._init, [opt, arg, str(self.src_path)])