Add Clone command and fixes
This commit is contained in:
		
							parent
							
								
									d55fc63a41
								
							
						
					
					
						commit
						f0a7624b0f
					
				@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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"""
 | 
				
			||||||
 | 
				
			|||||||
@ -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:
 | 
				
			||||||
 | 
				
			|||||||
@ -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)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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)])
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user