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.collection import Collection
from ordigi.geolocation import GeoLocation
from ordigi import utils
_logger_options = [
click.option(
@ -173,7 +174,7 @@ def _check(**kwargs):
if summary.errors:
sys.exit(1)
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)
@ -256,6 +257,39 @@ def _clean(**kwargs):
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')
@add_options(_logger_options)
@add_options(_dry_run_options)

View File

@ -3,6 +3,7 @@ Collection methods.
"""
from copy import copy
from datetime import datetime, timedelta
from distutils.dir_util import copy_tree
import filecmp
from fnmatch import fnmatch
import os
@ -299,6 +300,12 @@ class FileIO:
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):
if not self.dry_run:
directory.rmdir()
@ -699,14 +706,8 @@ class Collection(SortMedias):
if not 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.log = LOG.getChild(self.__class__.__name__)
# Get config options
self.opt = self.get_config_options()
@ -720,8 +721,11 @@ class Collection(SortMedias):
if not self.exclude:
self.exclude = set()
self.db = CollectionDb(root)
self.fileio = FileIO(self.opt['Terminal']['dry_run'])
self.root_is_valid()
self.db = CollectionDb(root)
self.paths = Paths(
self.opt['Filters'],
interactive=self.opt['Terminal']['interactive'],
@ -749,6 +753,16 @@ class Collection(SortMedias):
self.summary = Summary(self.root)
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):
"""Get collection config"""
config = Config(self.root.joinpath('.ordigi', 'ordigi.conf'))
@ -810,6 +824,14 @@ class Collection(SortMedias):
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):
if self.db.sqlite.is_empty('metadata'):
self.init(loc)
@ -817,6 +839,25 @@ class Collection(SortMedias):
self.log.error('Db data is not accurate run `ordigi update`')
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):
"""Update collection db"""
file_paths = list(self.get_collection_files())
@ -866,7 +907,7 @@ class Collection(SortMedias):
if checksum == self.db.sqlite.get_checksum(relpath):
self.summary.append('check', True, file_path)
else:
self.log.error('{file_path} is corrupted')
self.log.error(f'{file_path} is corrupted')
self.summary.append('check', False, file_path)
return self.summary

View File

@ -1,19 +1,13 @@
from datetime import datetime
import json
import os
from pathlib import Path
import sqlite3
import sys
from shutil import copyfile
from time import strftime
from ordigi import constants
from ordigi.utils import distance_between_two_points
class Sqlite:
"""Methods for interacting with Sqlite database"""
def __init__(self, target_dir):
@ -30,7 +24,7 @@ class Sqlite:
self.db_type = 'SQLite format 3'
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)
# Allow selecting column by name
self.con.row_factory = sqlite3.Row

View File

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

View File

@ -38,6 +38,10 @@ def distance_between_two_points(lat1, lon1, lat2, lon2):
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):
"""Return date regex generator"""
if user_regex:

View File

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

View File

@ -72,6 +72,8 @@ class TestOrdigi:
for command in commands:
self.assert_cli(command, ['not_exist'], state=1)
self.assert_cli(cli._clone, ['not_exist'], state=2)
def test_sort(self):
bool_options = (
# '--interactive',
@ -97,6 +99,20 @@ class TestOrdigi:
self.assert_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):
for opt, arg in self.logger_options:
self.assert_cli(cli._init, [opt, arg, str(self.src_path)])