Ejemplo n.º 1
0
def join_files(fpath, output_fpath, verbose, notify):
    """
    Concatenate multiple audio files with FFmpeg.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    assert len(fpath) > 1, 'Must pass more than one file to join!'
    vb = Verbose(verbose)

    fpaths = list(fpath)
    vb.info(
        f'Joining {len(fpaths)-1} additional audio files onto base file "{fpaths[0]}"...'
    )

    if output_fpath is None:
        output_fpath = pydoni.append_filename_suffix(fpaths[0], '-JOINED')

    audio = pydoni.AudioFile(audio_fpath=fpaths[0])
    ffmpeg_output = audio.join(additional_audio_files=fpaths[1:],
                               output_fpath=output_fpath)
    vb.info(f'Output audiofile created "{output_fpath}"')

    if notify:
        pydoni.macos_notify(title='Audiofile Join Complete!')

    outfile_size_in_bytes = stat(output_fpath).st_size
    result = dict(joined_files=fpaths,
                  output_fpath=output_fpath,
                  outfile_size_in_bytes=outfile_size_in_bytes,
                  ffmpeg_output=ffmpeg_output)

    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='audio.join_files'))
Ejemplo n.º 2
0
def remove_empty_subfolders(root, recursive, true_remove, count_hidden_files,
                            verbose):
    """
    Scan a directory and delete any empty bottom-level directories.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    dpath = list(root)
    vb = Verbose(verbose)

    removed_dirs = []
    for root in dpath:
        pydoni.remove_empty_subfolders(root=root,
                                       recursive=recursive,
                                       true_remove=true_remove,
                                       count_hidden_files=count_hidden_files)
        removed_dirs += root

    if verbose:
        if len(removed_dirs):
            for dir in removed_dirs:
                vb.info('Removed: ' + dir)
        else:
            vb.info('No empty directories found', level='warn')

    result['removed_dirs'] = removed_dirs
    pydoni.__pydonicli_register__(
        dict(args=args,
             result=result,
             command_name='opsys.remove_empty_subfolders'))
Ejemplo n.º 3
0
def to_mp3(fpath, verbose, notify):
    """
    Convert an audio file to .mp3 format.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)
    fpaths = list(fpath)

    output_fpaths = []
    for f in fpaths:
        vb.info(f'Converting file "{f}" to MP3')
        audio = pydoni.AudioFile(f)
        output_fpath = output_fpath = splitext(f)[0] + '-CONVERTED.mp3'
        audio.convert(output_format='mp3', output_fpath=output_fpath)
        vb.info(f'Outputted .mp3 file "{output_fpath}"')
        output_fpaths.append(output_fpath)

    if notify:
        pydoni.macos_notify(message='Audiofile compression complete',
                            title='Pydoni-CLI')

    result = {'fpaths': fpaths, 'output_fpaths': output_fpaths}
    result = dict(fpaths=fpaths)

    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='audio.to_mp3'))
Ejemplo n.º 4
0
def du_by_filetype(dpath, output_fpath, recursive, quiet, human_readable,
                   progress):
    """
    List the total filesize in a directory by file type.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    dpaths = list(dpath)
    write_lst = []

    for d in dpaths:
        write_lst.append(f'Directory "{d}"')
        filesize_dct = pydoni.du_by_filetype(dpath=d,
                                             recursive=recursive,
                                             human_readable=human_readable,
                                             progress=progress)

        for ftype, fsize in filesize_dct.items():
            write_lst.append(f'{ftype}: {fsize}')

    # Print to console
    if not quiet:
        for item in write_lst:
            print(item)

    # Write output file
    write_output = True if output_fpath is not None else False
    if write_output:
        with open(output_fpath, 'a') as f:
            for item in write_lst:
                f.write(item + '\n')

    result['result'] = filesize_dct
    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='opsys.du_by_filetype'))
Ejemplo n.º 5
0
def instagram_hashtags(austin,
                       sf,
                       sony,
                       landscape,
                       long,
                       night,
                       dji,
                       limit=28):
    """
    Create Instagram hashtag comment string given lists of instagram keywords. Allow user
    to choose which keyword groups to use, and to set a limit on the number of hashtags
    returned. Then generate a comment string from those selected keywords.
    """
    params = locals()
    options = {k: v for k, v in params.items() if k not in ['limit']}

    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    if sum([v for k, v in options.items()]) == 0:
        raise Exception(
            'Must select at least one option! Possibilities: {}'.format(
                [k for k, v in options.items()]))

    hashtags = define_hashtags()

    keyword_groups = list({k for k, v in params.items() if v is True})
    keywords = [v for k, v in hashtags.items() if k in keyword_groups]
    keywords = [item for sublist in keywords for item in sublist]
    random.shuffle(keywords)

    if limit < len(keywords):
        keywords = keywords[:limit]

    keywords = ['#' + x for x in keywords]

    spaces = []
    space_chars = ['·', '.', '-', '*', '~']
    for i in range(1, 6):
        spaces.append(''.join(random.choices(space_chars, k=1)) * i)

    keyword_string = '\n'.join(spaces) + '\n' + ' '.join(keywords)

    print('Here are your selected Instagram keywords:')
    print()
    print(keyword_string)

    result = {
        'keyword_groups': keyword_groups,
        'keyword_string': keyword_string
    }
    pydoni.__pydonicli_register__(
        dict(args=args, result=result,
             command_name='photo.instagram_hashtags'))
Ejemplo n.º 6
0
def text_to_speech(input, output_fpath, verbose, notify):
    """
    Convert raw text, either as commandline input or file input, to speech using gTTS.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)

    input = list(input)
    output = list(output_fpath)

    assert len(input), 'Must pass --input'
    assert len(output), 'Must pass --output'

    if output[0] is None:
        output = [None] * len(input)

    assert len(input) == len(
        output), '--input and --output-fpath must be of the same length'

    for i, o in zip(input, output):
        if isfile(i):
            # Input is file
            assert splitext(i)[1].lower(
            ) == '.txt', f'Input file "{i}" must have extension ".txt"'
            with open(i, 'r') as f:
                text_to_speechify = f.read()

            if o is None:
                # Default output file is the same as the input file but with .mp3 extension
                o = splitext(i)[0] + '.mp3'
        else:
            # Input is text
            text_to_speechify = i
            if o is None:
                # Default output file is a generic file on desktop
                o = join(
                    expanduser('~'), 'Desktop',
                    f'text_to_speech_{pydoni.systime(as_string=True)}.mp3')

        speech = gTTS(text=text_to_speechify, lang='en', slow=False)
        speech.save(o)

    if notify:
        pydoni.macos_notify(message='Text to speech complete',
                            title='Pydoni-CLI')

    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='audio.text_to_sp'))
Ejemplo n.º 7
0
def append_backup_log_table(table_schema,
                            table_name,
                            source,
                            source_size_bytes,
                            target,
                            target_size_before_bytes,
                            target_size_after_bytes,
                            start_ts,
                            end_ts,
                            is_completed,
                            verbose):
    """
    Append a record to directory backup Postgres table. To be used if a backup is carried
    out without the use of the `pydoni data backup` command which handles the table insert
    automatically, but when the backup would still like to be logged in the log table.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)
    pg = pydoni.Postgres()

    sql_value_dct = dict(source=source,
                         source_size_bytes=source_size_bytes,
                         target=target,
                         target_size_before_bytes=target_size_before_bytes,
                         target_size_after_bytes=target_size_after_bytes,
                         start_ts=datetime.datetime.fromtimestamp(start_ts),
                         end_ts=datetime.datetime.fromtimestamp(end_ts),
                         is_completed=is_completed)

    vb.info(f'table_schema: {table_schema}')
    vb.info(f'table_name: {table_name}')
    for k, v in sql_value_dct.items():
        vb.info(f'{k}: {v}')

    insert_sql = pg.build_insert(schema_name=table_schema,
                                 table_name=table_name,
                                 columns=[k for k, v in sql_value_dct.items()],
                                 values=[v for k, v in sql_value_dct.items()])

    pg.execute(insert_sql)

    vb.info(f'Appended record to {table_schema}.{table_name}')
    result['sql_value_dct'] = sql_value_dct

    vb.program_complete('Append to backup log table complete')

    pydoni.__pydonicli_register__(dict(args=args, result=result, command_name='data.append_backup_log_table'))
Ejemplo n.º 8
0
def workflow(source_dpath, pg_schema, sample, full_rebuild, verbose, dry_run,
             no_startup_message, no_pipeline):
    """
    Refresh Postgres Photo schema from source photo metadata.
    """
    args = pydoni.__pydonicli_declare_args__(locals())
    pydoni.__pydonicli_register__({
        'command_name':
        pydoni.what_is_my_name(with_modname=True),
        'args':
        args
    })

    # Begin pipeline stopwatch
    start_ts = time.time()

    # Set up variables used throughout entire pipeline
    vb = Verbose(verbose)
    pg = pydoni.Postgres()

    if vb.verbose:
        if not no_startup_message:
            print_startup_message()

    # Extract source media file metadata and load into Postgres
    pipeline_args = dict(pg=pg,
                         vb=vb,
                         source_dpath=source_dpath,
                         pg_schema=pg_schema,
                         sample=sample,
                         full_rebuild=full_rebuild)

    if not no_pipeline:
        source_to_postgres_pipeline(**pipeline_args)

    # Apply transformations on data once loaded into Postgres
    db_transform(pg, vb, pg_schema, sample)

    # End workflow
    pydoni.__pydonicli_register__(
        {k: v
         for k, v in locals().items() if k in ['result']})
    msg = f'Photo database refresh complete'
    vb.program_complete(msg, start_ts=start_ts)
Ejemplo n.º 9
0
def split_batch_exported_timelapse(dpath):
    """
    Split a directory of exported timelapse stills into their respective folders.

    Accept a directory of exported timelapse stills from Lightroom following Andoni's
    photo file naming convention. Those timelapse files will be in sequences numbered
    from 1 to the final # of the timelapse series, then will reset to 1 for the next
    timelapse. There could be an arbitrary number of timelapse stills in this directory.

    This program will take all the files in that directory and split them into folders
    for easy categorization.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    os.chdir(dpath)

    files = pydoni.listfiles(ext='jpg')
    assert len(files), "No timelapse stills found!"

    seqnums = [int(os.path.splitext(f.split('_')[4])[0]) for f in files]
    differences = []
    for i, num in enumerate(seqnums):
        last_idx = i - 1
        last_idx = last_idx if last_idx >= 0 else 0
        last_num = seqnums[last_idx]
        differences.append(num - last_num)

    delimiters = [i for i, x in enumerate(differences) if x not in [0, 1]]
    files_list_of_lists = pydoni.split_at(files, delimiters)

    for i, list_of_files in enumerate(files_list_of_lists):
        dname = 'timelapse_%s_of_%s' % (str(i + 1),
                                        str(len(files_list_of_lists)))
        if not os.path.isdir(dname):
            os.mkdir(dname)
        for fname in list_of_files:
            newfname = os.path.join(dname, fname)
            os.rename(fname, newfname)

    result['directories_created'] = len(files_list_of_lists)
    pydoni.__pydonicli_register__(
        dict(args=args,
             result=result,
             command_name='photo.split_batch_exported_timelapse'))
Ejemplo n.º 10
0
def ocr(fpath, verbose):
    """
    OCR an image using pytesseract.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)

    fpaths = list(fpath)
    for fpath in fpaths:
        vb.info(f'Applying OCR to file "{fpath}"...')
        text = pydoni.ocr_image(fpath)

        with open(splitext(fpath)[0] + '.txt', 'w') as f:
            f.write(text)

        vb.info("Successfully OCR'd file")

    result['ocr_files'] = fpaths
    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='image.ocr'))
Ejemplo n.º 11
0
def compress(fpath, verbose, notify):
    """
    Compress one or more audiofiles.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)

    fpaths = list(fpath)
    for fpath in fpaths:
        ffmpeg_output = pydoni.AudioFile(fpath).compress()
        vb.info(f'Successfully compressed file "{fpath}"')

    if notify:
        pydoni.macos_notify(title='M4A to MP3 Conversion Complete!')

    result['compressed_files'] = fpaths
    result['ffmpeg_output'] = ffmpeg_output

    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='audio.compress'))
Ejemplo n.º 12
0
def to_gif(fpath, verbose, notify):
    """
    Convert video file(s) to GIF.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)
    fpaths = list(fpath)

    for f in fpaths:
        gif_fpath = os.path.splitext(f)[0] + '.gif'
        pydoni.video_to_gif(video_fpath=f, gif_fpath=gif_fpath, fps=10)
        vb.echo(f'Outputted .gif file "{gif_fpath}"')
        result[f] = gif_fpath

    if notify:
        pydoni.macos_notify(message='Conversion complete',
                            title='Video to GIF')

    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='video.to_gif'))
Ejemplo n.º 13
0
def refresh_imdb_table(schema_name, table_name, omdbapikey, verbose=False):
    """
    Query Postgres table containing IMDB metadata and refresh any values that need updating.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    # 'result' will be a dictionary where the movie names are the keys, and the values are
    # dictionaries with items: 'status', 'message', 'updated_values' (dictionary of
    # updated values, if any).
    result_items = ['status', 'message', 'updated_values']

    pg = pydoni.Postgres()
    pkey_name = 'movie_id'
    df = pg.read_table(schema_name, table_name).sort_values(pkey_name)
    cols = pg.col_names(schema_name, table_name)

    if verbose:
        pbar = tqdm(total=len(df), unit='movie')

    for i, row in df.iterrows():
        movie_name = f"{row['title']} ({str(row['release_year'])})"

        try:
            omdbresp = query_omdb(title=row['title'],
                                  release_year=row['release_year'],
                                  omdbapikey=omdbapikey)

        except Exception as e:
            err_str = f"{click.style('ERROR', fg='red')} in {movie_name}: {str(e)}"
            tqdm.write(err_str)

            result[movie_name] = {
                k: v
                for k, v in zip(result_items, ['Error', str(e), None])
            }

            if verbose:
                pbar.update(1)

            continue

        omdbresp = {k: v for k, v in omdbresp.items() if k in cols}
        omdbresp = {k: replace_null(v) for k, v in omdbresp.items()}

        color_map = {
            'No change': 'yellow',
            'Updated': 'green',
            'Not found': 'red'
        }
        change = 'Not found' if not len(omdbresp) else 'No change'

        # Filter out columns and values that do not require an update
        if change != 'Not found':
            upd = filter_updated_values(omdbresp, row)
            change = 'Updated' if len(upd) else change
            upd['imdb_update_ts'] = datetime.datetime.now()

            stmt = pg.build_update(schema_name,
                                   table_name,
                                   pkey_name=pkey_name,
                                   pkey_value=row[pkey_name],
                                   columns=[k for k, v in upd.items()],
                                   values=[v for k, v in upd.items()],
                                   validate=True)
            pg.execute(stmt)

            upd_backend = {
                k: v
                for k, v in upd.items() if k != 'imdb_update_ts'
            }
            upd_backend = upd_backend if len(upd_backend) else None
            result[movie_name] = {
                k: v
                for k, v in zip(result_items, [change, None, upd_backend])
            }

        else:
            result[movie_name] = {
                k: v
                for k, v in zip(result_items, [change, None, None])
            }

        if verbose:
            pbar.update(1)
            space = '  ' if change == 'Updated' else ''
            tqdm.write(
                click.style(change, fg=color_map[change]) + space + ': ' +
                movie_name)

    if verbose:
        pbar.close()
        pydoni.program_complete('Movie refresh complete!')

    pydoni.__pydonicli_register__(
        dict(args=args, result=result,
             command_name='movie.refresh_imdb_table'))
Ejemplo n.º 14
0
def website_extract_image_titles(website_export_dpath, outfile, verbose):
    """
    Scan photo files exported for andonisooklaris.com and construct list of image filenames
    and titles, separated by collection.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    def echo(*args, **kwargs):
        kwargs['timestamp'] = True
        pydoni.echo(*args, **kwargs)

    website_export_dpath = expanduser(website_export_dpath)
    if outfile == 'auto':
        outfile = os.path.join(
            website_export_dpath,
            'Image Titles %s.txt' % pydoni.sysdate(stripchars=True))
    elif outfile is not None:
        assert not os.path.isfile(outfile)

    files = pydoni.listfiles(path=website_export_dpath,
                             recursive=True,
                             full_names=True)
    files = [f for f in files if os.path.splitext(f)[1].lower() != '.txt']

    if verbose:
        echo('Files found: ' + str(len(files)))
        echo('Extracting EXIF metadata...')
        exifd = pydoni.EXIF(files).extract()
        echo('EXIF metadata successfully extracted')

        if outfile is not None:
            echo('Writing output datafile: ' + outfile)
    else:
        exifd = pydoni.EXIF(files).extract()

    i = 0
    tracker = pd.DataFrame(columns=['collection', 'file', 'title'])
    for file in files:
        elements = file.replace(website_export_dpath,
                                '').lstrip('/').split('/')
        subcollection = None
        collection = elements[0]
        fname = elements[-1]

        if len(elements) == 3:
            subcollection = elements[1]
            collection += ' - ' + subcollection

        exif = exifd[os.path.join(website_export_dpath, file)]
        title = exif['Title'] if 'Title' in exif.keys() else ''
        year = fname[0:4]
        title = str(year) + ' ' + str(title)

        tracker.loc[i] = [collection, fname, title]
        i += 1

    print_lst = []
    for collection in tracker['collection'].unique():
        print_lst.append('\nCollection: %s\n' % collection)
        df_print = tracker.loc[tracker['collection'] == collection].drop(
            'collection', axis=1)
        print_lst.append(
            tabulate(df_print, showindex=False, headers=df_print.columns))

    print_str = '\n'.join(print_lst).strip()
    if outfile is None:
        print(print_str)
    else:
        with open(outfile, 'w') as f:
            f.write(print_str)

    if verbose:
        pydoni.program_complete()

    result['n_collections'] = len(tracker['collection'].unique())
    pydoni.__pydonicli_register__(
        dict(args=args,
             result=result,
             command_name='photo.website_extract_image_titles'))
Ejemplo n.º 15
0
def rename_mediafile(fpath, initials, tz_adjust, verbose, notify):
    """
    Rename a photo or video file according to a specified file naming convention.

    fpath {str} or {list}: filename or list of filenames to rename
    initials {str}: 2 or 3 letter initials string
    notify {bool}: execute `pydoni.opsys.macos_notify()` on program completion
    tz_adjust {int}: adjust file creation times by a set number of hours
    verbose {bool}: print messages and progress bar to console
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    assert len(initials) in [2,
                             3], 'Initials must be a 2- or 3-character string'

    vb = Verbose(verbose)
    mediafiles = list(fpath)

    assert len(mediafiles), 'No mediafiles to rename!'
    assert isinstance(mediafiles[0], str), \
        f'First element of variable `mediafiles` is of type {type(mediafiles[0]).__name__}, expected string'

    CONV = Convention()
    EXT = Extension()

    mediafiles = [os.path.abspath(x) for x in mediafiles \
        if not re.match(CONV.photo, os.path.basename(x)) \
        and not re.match(CONV.video, os.path.basename(x))]

    msg = f'Renaming {len(mediafiles)} media files'
    if verbose:
        vb.section_header(msg)
        pbar = tqdm(total=len(mediafiles), unit='mediafile')

    if not len(mediafiles):
        if verbose:
            pydoni.echo('No files to rename!', fg='green')

    for mfile in mediafiles:
        if verbose:
            vb.stabilize_postfix(mfile, max_len=15)

        mf = MediaFile(mfile)
        newfname = mf.build_fname(initials=initials, tz_adjust=tz_adjust)
        newfname = os.path.join(os.path.dirname(mfile),
                                os.path.basename(newfname))

        if os.path.basename(mfile) != os.path.basename(newfname):
            os.rename(mfile, newfname)
            result[os.path.basename(mfile)] = os.path.basename(newfname)
            if verbose:
                tqdm.write('{}: {} -> {}'.format(
                    click.style('Renamed', fg='green'),
                    os.path.basename(mfile), os.path.basename(newfname)))
        else:
            result[os.path.basename(
                mfile)] = '<not renamed, new filename identical>'
            if verbose:
                tqdm.write('{}: {}'.format(
                    click.style('Not renamed', fg='red'),
                    os.path.basename(mfile)))

        if verbose:
            pbar.update(1)

    if verbose:
        pbar.close()
        pydoni.echo(f'Renamed media files: {len(mediafiles)}', indent=2)

    if verbose or notify:
        pydoni.macos_notify(title='Mediafile Rename',
                            message='Completed successfully!')

    pydoni.__pydonicli_register__(
        dict(args=args, result=result, command_name='photo.rename_mediafile'))
Ejemplo n.º 16
0
def backup(source, target, update_log_table, use_rsync, verbose, debug, dry_run):
    """
    Back up a source directory to a target directory.

    This function will accept a source and target directories, most often
    on separate external hard drives, and copy all files from the source
    to the target that are either:

        (1) Not in the target directory
        (2) Are in the target directory, but have been updated

    Files in the target that have been deleted in the source will also be deleted.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    start_ts = time.time()
    vb = Verbose(verbose=verbose, debug=debug)
    ws = '  '

    ignore_files = [
        'The Office S09E16 Moving On.mkv',
        'The Office S09E20 Paper Airplanes.mkv',
    ]

    if update_log_table:
        start_ts_utc = datetime.datetime.utcnow()
        pg = pydoni.Postgres()
        directory_backup_table_schema = 'pydonicli'
        directory_backup_table_name = 'directory_backup'

        insert_dict = dict(source=source,
                           source_size_bytes=stat(source).st_size,
                           target=target,
                           target_size_before_bytes=stat(target).st_size,
                           target_size_after_bytes=None,
                           start_ts=start_ts_utc,
                           is_completed=False)

        insert_sql = pg.build_insert(schema_name=directory_backup_table_schema,
                                     table_name=directory_backup_table_name,
                                     columns=list(insert_dict.keys()),
                                     values=list(insert_dict.values()),
                                     validate=True)
        if not dry_run:
            pg.execute(insert_sql)

        directory_backup_id = pg.read_sql(f"""
        select directory_backup_id
        from {directory_backup_table_schema}.{directory_backup_table_name}
        order by gen_ts desc
        limit 1""").squeeze()


    assert source != target, 'Source and target directories must be different'

    if use_rsync:
        cmd_lst = ['rsync', '--delete-before', '-a', '-h', '-u']
        if verbose:
            cmd_lst = cmd_lst + ['-v', '--progress']

        cmd_lst = cmd_lst + [f'"{source}"'] + [f'"{target}"']
        cmd = ' '.join(cmd_lst)

        subprocess.call(cmd, shell=True)

        # progress_flag = ' --progress' if verbose else ''
        # backup_cmd = f'rsync -avhu{progress_flag} --delete-before "{source}" "{target}"'
        # subprocess.call(backup_cmd, shell=True)

    else:
        vb.info(f'Listing files at source: {source}')
        files_source = pydoni.listfiles(path=source, recursive=True, full_names=True)
        vb.debug('Found files at source: ' + str(len(files_source)))
        files_source = [x for x in files_source if x not in ignore_files]
        vb.debug(f'Found files at source after filtering out manually ignored files: {len(files_source)}')

        vb.info(f'Listing files at target: {target}')
        files_target = pydoni.listfiles(path=target, recursive=True, full_names=True)
        vb.debug('Found files at target: ' + str(len(files_target)))
        files_target = [x for x in files_target if x not in ignore_files]
        vb.debug(f'Found files at target after filtering out manually ignored files: {len(files_target)}')

        # Scan source files and for each determine whether to do nothing, copy to target,
        # or replace at target
        copied_files = []
        replaced_files = []
        vb.info('Scanning for new, updated or deleted files at source')
        vb.pbar_init(total=len(files_source), unit='file')

        for sourcefile in files_source:
            vb.pbar_write(f'Sourcefile: {sourcefile}', refer_debug=True)
            vb.pbar.set_postfix({'file': basename(sourcefile)})

            targetfile = sourcefile.replace(source, target)
            vb.pbar_write(f'{ws}Expected mirrored targetfile: {targetfile}', refer_debug=True)

            if not isfile(targetfile):
                # Copy file to target. Create parent directory at target if not exists
                vb.pbar_write(f'{ws}(Copy) attempting to copy file "{sourcefile}" to "{targetfile}"', refer_debug=True)

                targetdpath = dirname(targetfile)
                if not isdir(targetdpath):
                    vb.pbar_write(f'{ws}{ws}Parent directory of targetfile does not exist, creating it at: ' + targetdpath, refer_debug=True)
                    if not dry_run:
                        makedirs(targetdpath)

                    vb.pbar_write(f'{ws}{ws}Successful', refer_debug=True)

                if not dry_run:
                    shutil.copy2(sourcefile, targetfile)

                vb.pbar_write(f'{ws}Successful', refer_debug=True)
                copied_files.append(sourcefile)

            elif isfile(targetfile) and is_file_changed(sourcefile, targetfile):
                # Replace file at target (same action as copy, but parent directory must exist)
                vb.pbar_write(f'(Replace) attempting to copy file "{sourcefile}" to "{targetfile}"', refer_debug=True)
                if not dry_run:
                    shutil.copy2(sourcefile, targetfile)

                vb.pbar_write(f'Successful', refer_debug=True)
                replaced_files.append(sourcefile)

            else:
                vb.pbar_write(f'{ws}Targetfile already exists and is unchanged', refer_debug=True)

            vb.pbar_update(1)

        vb.pbar_close()

        # Scam target files and for each determine whether that file has been since
        # deleted from source
        deleted_files = []
        vb.info('Scanning for files at target since deleted from source')
        vb.pbar_init(total=len(files_target))
        for targetfile in files_target:
            sourcefile = targetfile.replace(target, source)
            vb.pbar.set_postfix({'file': basename(targetfile)})

            if not isfile(sourcefile) and not isdir(sourcefile):
                vb.pbar_write(f'(Delete) attempting to delete "{targetfile}"', refer_debug=True)
                if not dry_run:
                    send2trash(targetfile)

                vb.pbar_write(f'{ws}Successful', refer_debug=True)
                deleted_files.append(targetfile)

            vb.pbar_update(1)

        vb.pbar_close()

        # Record number of files copied, replaced and deleted
        vb.info(f'Copied {len(copied_files)} files')
        vb.info(f'Replaced {len(replaced_files)} files')
        vb.info(f'Deleted {len(deleted_files)} files')
        vb.info(f'Unchanged {len(files_source) - len(copied_files) - len(replaced_files) - len(deleted_files)} files')
        result = dict(copied=len(copied_files),
                      replaced=len(replaced_files),
                      deleted=len(deleted_files),
                      unchanged=len(files_source) - len(copied_files) - len(replaced_files) - len(deleted_files))

    if update_log_table:
        vb.debug('Attempting to update log table with results...')

        update_dict = dict(target_size_after_bytes=pydoni.dirsize(target),
                           end_ts=datetime.datetime.utcnow(),
                           is_completed=True)

        update_sql = pg.build_update(schema_name=directory_backup_table_schema,
                                     table_name=directory_backup_table_name,
                                     pkey_name='directory_backup_id',
                                     pkey_value=directory_backup_id,
                                     columns=list(update_dict.keys()),
                                     values=list(update_dict.values()),
                                     validate=True)

        if not dry_run:
            pg.execute(update_sql)

        vb.debug(f'{ws}Successful')

    vb.program_complete('Backup complete', start_ts=start_ts)
    pydoni.__pydonicli_register__(dict(args=args, result=result, command_name='data.backup'))
Ejemplo n.º 17
0
def pg_dump(backup_dir,
            db_name,
            pg_user,
            sep,
            pgdump,
            csvdump,
            max_dir_size,
            dry_run,
            verbose):
    """
    Dump a local Postgres database. Looks for ~/.pgpass by default.
    """
    args, result = pydoni.__pydonicli_declare_args__(locals()), dict()

    vb = Verbose(verbose)

    if dry_run:
        vb.info('Not executing any code (dry run)')

    if pg_user is not None and db_name is not None:
        pg = pydoni.Postgres(pg_user=pg_user, db_name=db_name)
    else:
        # Attempt to parse ~/.pgpass file. Fail if this file does not exist or is not
        # able to be parsed
        pg = pydoni.Postgres()

    # Define subfolder to dump files to within dump directory
    subdir = pydoni.systime(compact=True) + '_' + pg.db_name
    backup_subdir = join(expanduser(backup_dir), subdir)
    mkdir(backup_subdir)

    vb.info('Database: ' + pg.db_name)
    vb.info('Destination folder: ' + backup_subdir)

    # Dump database based on user's preference
    # May dump using pg_dump, export tables to CSV, or both

    dumped_files = []

    if pgdump:
        vb.info('Executing `pg_dump`')
        if not dry_run:
            dumped_dbfile = pg.dump(backup_dir=backup_subdir)
            dumped_files += [dumped_dbfile]

    if csvdump:
        # Dump each file to textfile
        vb.info('Executing CSV dump to tables')
        if not dry_run:
            dumped_csvfiles = pg.dump_tables(backup_dir=backup_subdir, sep=sep, coerce_csv=False)
            dumped_files += dumped_csvfiles

    result['backup_directory'] = backup_subdir
    result['dumped_files'] = {}
    for f in dumped_files:
        result['dumped_files'][basename(f)] = dict(
            filesize=stat(f).st_size,
            filesize_readable=pydoni.human_filesize(stat(f).st_size),
            created=datetime.datetime.fromtimestamp(getctime(f)).strftime('%Y-%m-%d %H:%M:%S.%f'),
            rows=pydoni.textfile_len(f))

    if verbose:
        vb.line_break()
        tt_list = [[basename(file),
                    infodict['created'],
                    pydoni.human_filesize(infodict['filesize']),
                    str(infodict['rows'])
                   ] for file, infodict in result['dumped_files'].items()]

        if len(tt_list):
            if verbose:
                print(tt.to_string(
                    tt_list,
                    header=[click.style(x, bold=True) for x in ['File', 'Created', 'Size', 'Rows']],
                    style=tt.styles.ascii_thin_double,
                    padding=(0, 1),
                    alignment='ccrr'))
        else:
            vb.warn('No database files were dumped!')

    if dry_run:
        rmdir(backup_subdir)

    max_dir_size_enforced = False
    removed_old_backup_dirs = []
    if max_dir_size:
        # Check size of `backup_dir` and clear any backup directories until the total size
        # is less than max_dir_size (upper GB limit)
        subdirs = sorted([x for x in pathlib.Path(backup_dir).iterdir() if isdir(x)], key=getmtime)
        subdirs_size = zip(subdirs, [pydoni.dirsize(x) / 1e9 for x in subdirs])
        total_size = sum([y for x, y in subdirs_size])

        if total_size > max_dir_size:
            vb.warn(f'Enforcing maximum directory size: {str(max_dir_size)} GB')
            max_dir_size_enforced = True

        while total_size > max_dir_size:
            dir_to_remove = str(subdirs[0])
            shutil.rmtree(dir_to_remove)
            removed_old_backup_dirs.append(dir_to_remove)

            subdirs = sorted([x for x in pathlib.Path(backup_dir).iterdir() if isdir(x)], key=getmtime)

            subdirs_size = zip(subdirs, [pydoni.dirsize(x) / 1e9 for x in subdirs])
            total_size = sum([y for x, y in subdirs_size])

            vb.warn(f'Removed "{basename(dir_to_remove)}"')

    vb.program_complete('Postgres dump complete')

    result['max_dir_size_enforced'] = max_dir_size_enforced
    result['removed_old_backup_dirs'] = [basename(x) for x in removed_old_backup_dirs]

    pydoni.__pydonicli_register__(dict(args=args, result=result, command_name='data.pg_dump'))