skilleter-thingy 0.1.19__tar.gz → 0.1.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skilleter-thingy might be problematic. Click here for more details.
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/PKG-INFO +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/pyproject.toml +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/ggrep.py +2 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_br.py +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_parent.py +2 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_review.py +2 -2
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_update.py +2 -2
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/gphotosync.py +9 -5
- skilleter_thingy-0.1.22/skilleter_thingy/localphotosync.py +191 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/multigit.py +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/photodupe.py +10 -10
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/py_audit.py +4 -2
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/readable.py +13 -11
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/sysmon.py +2 -2
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/colour.py +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/dircolors.py +20 -18
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/git2.py +0 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/popup.py +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/tfm_pane.py +3 -3
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/tidy.py +14 -13
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/venv_create.py +1 -1
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy.egg-info/PKG-INFO +1 -1
- skilleter_thingy-0.1.22/skilleter_thingy.egg-info/PKG-INFO 2 +193 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy.egg-info/SOURCES.txt +1 -0
- skilleter_thingy-0.1.19/skilleter_thingy/localphotosync.py +0 -471
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/LICENSE +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/README.md +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/setup.cfg +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/__init__.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/addpath.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/borger.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/console_colours.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/diskspacecheck.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/docker_purge.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/ffind.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/ggit.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_ca.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_cleanup.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_co.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_common.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_hold.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_mr.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_retag.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/git_wt.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/gitcmp_helper.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/gitprompt.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/gl.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/linecount.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/moviemover.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/phototidier.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/remdir.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/rmdupe.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/rpylint.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/splitpics.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/strreplace.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/tfm.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/tfparse.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/__init__.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/dc_curses.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/dc_defaults.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/dc_util.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/docker.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/files.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/git.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/gitlab.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/path.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/process.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/run.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/thingy/venv_template.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/trimpath.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/window_rename.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/xchmod.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy/yamlcheck.py +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy.egg-info/entry_points.txt +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy.egg-info/requires.txt +0 -0
- {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.22}/skilleter_thingy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skilleter_thingy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.22
|
|
4
4
|
Summary: A collection of useful utilities, mainly aimed at making Git more friendly
|
|
5
5
|
Author-email: John Skilleter <john@skilleter.org.uk>
|
|
6
6
|
Project-URL: Home, https://skilleter.org.uk
|
|
@@ -39,7 +39,8 @@ def parse_command_line():
|
|
|
39
39
|
parser.add_argument('--files-with-matches', '-l', action='store_true', help='Show only the names of files that contain matches')
|
|
40
40
|
parser.add_argument('--files-without-matches', '-L', action='store_true', help='Show only the names of files that do NOT contain matches')
|
|
41
41
|
parser.add_argument('--wildcard', '-W', action='append', help='Only search files matching the wildcard(s)')
|
|
42
|
-
parser.add_argument('--only-matching', '-o', action='store_true',
|
|
42
|
+
parser.add_argument('--only-matching', '-o', action='store_true',
|
|
43
|
+
help='Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line.')
|
|
43
44
|
parser.add_argument('--no-color', action='store_true', help='Turn off match highlighting')
|
|
44
45
|
parser.add_argument('pattern', action='store', help='Regular expression to search for')
|
|
45
46
|
parser.add_argument('paths', nargs='*', help='Optional list of one or more paths to search')
|
|
@@ -23,7 +23,8 @@ def main():
|
|
|
23
23
|
parser = argparse.ArgumentParser(description='Attempt to determine the parent branch for the specified branch (defaulting to the current one)')
|
|
24
24
|
parser.add_argument('--all', '-a', action='store_true', help='Include feature branches as possible parents')
|
|
25
25
|
parser.add_argument('--verbose', '-v', action='store_true', help='Report verbose results (includes number of commits between branch and parent)')
|
|
26
|
-
parser.add_argument('branch', action='store', nargs='?', type=str, default=current_branch,
|
|
26
|
+
parser.add_argument('branch', action='store', nargs='?', type=str, default=current_branch,
|
|
27
|
+
help=f'Branch, commit or commit (defaults to current branch; {current_branch})')
|
|
27
28
|
|
|
28
29
|
args = parser.parse_args()
|
|
29
30
|
|
|
@@ -448,7 +448,7 @@ class GitReview():
|
|
|
448
448
|
self.filter_modified = pickle_data.get('filter_modified', self.filter_modified)
|
|
449
449
|
self.sort_order = pickle_data.get('sort_order', self.sort_order)
|
|
450
450
|
self.reverse_sort = pickle_data.get('reverse_sort', self.reverse_sort)
|
|
451
|
-
self.filter_none_whitespace_only= pickle_data.get('filter_none_whitespace_only', self.filter_none_whitespace_only)
|
|
451
|
+
self.filter_none_whitespace_only = pickle_data.get('filter_none_whitespace_only', self.filter_none_whitespace_only)
|
|
452
452
|
self.show_none_whitespace_stats = pickle_data.get('show_none_whitespace_stats', self.show_none_whitespace_stats)
|
|
453
453
|
|
|
454
454
|
# Transfer the reviewed flag for each file in the pickle
|
|
@@ -460,7 +460,7 @@ class GitReview():
|
|
|
460
460
|
newfile['reviewed'] = oldfile['reviewed']
|
|
461
461
|
break
|
|
462
462
|
|
|
463
|
-
except (EOFError, pickle.UnpicklingError, ModuleNotFoundError, AttributeError):
|
|
463
|
+
except (EOFError, pickle.UnpicklingError, ModuleNotFoundError, AttributeError): # TODO: Why did I get ModuleNotFoundError or AttributeError????
|
|
464
464
|
pass
|
|
465
465
|
|
|
466
466
|
self.__constrain_display_parameters()
|
|
@@ -85,7 +85,7 @@ def branch_rebase(args, results, branch):
|
|
|
85
85
|
if args.all_parents:
|
|
86
86
|
parents, _ = git.parents()
|
|
87
87
|
else:
|
|
88
|
-
parents, _ = git.parents(ignore='feature/*'
|
|
88
|
+
parents, _ = git.parents(ignore='feature/*')
|
|
89
89
|
|
|
90
90
|
logging.debug('Probable parents of %s: %s', branch, parents)
|
|
91
91
|
|
|
@@ -314,7 +314,7 @@ def main():
|
|
|
314
314
|
|
|
315
315
|
# List of stuff that's been done, to report in the summary
|
|
316
316
|
|
|
317
|
-
results = {'deleted': set(), 'pulled': set(), 'failed': set(), 'rebased': set(), 'unchanged': set(), 'no-tracking': set()
|
|
317
|
+
results = {'deleted': set(), 'pulled': set(), 'failed': set(), 'rebased': set(), 'unchanged': set(), 'no-tracking': set()}
|
|
318
318
|
|
|
319
319
|
to_rebase = set()
|
|
320
320
|
|
|
@@ -111,15 +111,19 @@ def parse_command_line():
|
|
|
111
111
|
|
|
112
112
|
parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose status information')
|
|
113
113
|
parser.add_argument('--dryrun', '-D', action='store_true', help='Just list files to be copied, without actually copying them')
|
|
114
|
-
parser.add_argument('--picturedir', '-P', action='store', default=DEFAULT_PHOTO_DIR,
|
|
115
|
-
|
|
114
|
+
parser.add_argument('--picturedir', '-P', action='store', default=DEFAULT_PHOTO_DIR,
|
|
115
|
+
help=f'Location of local picture storage directory (defaults to {DEFAULT_PHOTO_DIR})')
|
|
116
|
+
parser.add_argument('--videodir', '-V', action='store', default=DEFAULT_VIDEO_DIR,
|
|
117
|
+
help=f'Location of local video storage directory (defaults to {DEFAULT_VIDEO_DIR})')
|
|
116
118
|
parser.add_argument('--start', '-s', action='store', default=None, help='Start date (in the form YYYY-MM, defaults to current month)')
|
|
117
119
|
parser.add_argument('--end', '-e', action='store', default=None, help=f'End date (in the form YYYY-MM, defaults to {DEFAULT_MONTHS} before the start date)')
|
|
118
120
|
parser.add_argument('--months', '-m', action='store', type=int, default=None, help='Synchronise this number of months of data (current month included)')
|
|
119
121
|
parser.add_argument('--cache', '-c', action='store', default=DEFAULT_CACHE_DIR, help=f'Cache directory for Google photos (defaults to {DEFAULT_CACHE_DIR})')
|
|
120
|
-
parser.add_argument('--rclone', '-r', action='store', default=DEFAULT_RCLONE_REMOTE,
|
|
122
|
+
parser.add_argument('--rclone', '-r', action='store', default=DEFAULT_RCLONE_REMOTE,
|
|
123
|
+
help=f'rclone remote name for Google photos (defaults to {DEFAULT_RCLONE_REMOTE})')
|
|
121
124
|
parser.add_argument('--no-update', '-N', action='store_true', help='Do not update local cache')
|
|
122
|
-
parser.add_argument('--keep', '-k', action='store', type=int, default=DEFAULT_KEEP,
|
|
125
|
+
parser.add_argument('--keep', '-k', action='store', type=int, default=DEFAULT_KEEP,
|
|
126
|
+
help=f'Keep this number of months before the start date in the cache (defaults to {DEFAULT_KEEP})')
|
|
123
127
|
parser.add_argument('--skip-no-day', '-z', action='store_true', help='Don\'t sync files where the day of the month could not be determined')
|
|
124
128
|
parser.add_argument('action', nargs='*', help='Actions to perform (report or sync)')
|
|
125
129
|
|
|
@@ -450,7 +454,7 @@ def remove_duplicates(media_files):
|
|
|
450
454
|
# Originals can have upper or lower case extensions, copies only tend to have lower
|
|
451
455
|
# case, so build a lower case to original lookup table
|
|
452
456
|
|
|
453
|
-
names = {name.lower():name for name in media_files}
|
|
457
|
+
names = {name.lower(): name for name in media_files}
|
|
454
458
|
|
|
455
459
|
duplicates = defaultdict(list)
|
|
456
460
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Sync a directory tree full of photos into a tree organised by year, month and date
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# TODO: Ignore patterns for source and destination file paths (.trashed* and .stversions)
|
|
8
|
+
# TODO: Use inotify to detect changes and run continuously
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import glob
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
import logging
|
|
15
|
+
import argparse
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
################################################################################
|
|
21
|
+
|
|
22
|
+
# Default locations for local storage of photos and videos
|
|
23
|
+
|
|
24
|
+
DEFAULT_PHOTO_DIR = os.path.expanduser('~/Pictures')
|
|
25
|
+
DEFAULT_VIDEO_DIR = os.path.expanduser('~/Videos')
|
|
26
|
+
|
|
27
|
+
# File extensions (case-insensitive)
|
|
28
|
+
|
|
29
|
+
IMAGE_EXTENSIONS = ('.jpg', '.jpeg', '.png')
|
|
30
|
+
VIDEO_EXTENSIONS = ('.mp4', '.mov')
|
|
31
|
+
|
|
32
|
+
# Enum of filetypes
|
|
33
|
+
|
|
34
|
+
class FileType(Enum):
|
|
35
|
+
"""File types"""
|
|
36
|
+
IMAGE = 0
|
|
37
|
+
VIDEO = 1
|
|
38
|
+
UNKNOWN = 2
|
|
39
|
+
IGNORE = 3
|
|
40
|
+
|
|
41
|
+
################################################################################
|
|
42
|
+
|
|
43
|
+
def error(msg, status=1):
|
|
44
|
+
"""Exit with an error message"""
|
|
45
|
+
|
|
46
|
+
print(msg)
|
|
47
|
+
sys.exit(status)
|
|
48
|
+
|
|
49
|
+
################################################################################
|
|
50
|
+
|
|
51
|
+
def parse_command_line():
|
|
52
|
+
"""Parse and validate the command line options"""
|
|
53
|
+
|
|
54
|
+
parser = argparse.ArgumentParser(description='Sync photos from Google Photos')
|
|
55
|
+
|
|
56
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose status information')
|
|
57
|
+
parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Just list files to be copied, without actually copying them')
|
|
58
|
+
parser.add_argument('--picturedir', '-P', action='store', default=DEFAULT_PHOTO_DIR,
|
|
59
|
+
help=f'Location of local picture storage directory (defaults to {DEFAULT_PHOTO_DIR})')
|
|
60
|
+
parser.add_argument('--videodir', '-V', action='store', default=DEFAULT_VIDEO_DIR,
|
|
61
|
+
help=f'Location of local video storage directory (defaults to {DEFAULT_VIDEO_DIR})')
|
|
62
|
+
parser.add_argument('--path', '-p', action='store', default=None, help='Path to sync from')
|
|
63
|
+
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
|
|
66
|
+
if not args.path:
|
|
67
|
+
error('You must specify a source directory')
|
|
68
|
+
|
|
69
|
+
# Configure debugging
|
|
70
|
+
|
|
71
|
+
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
|
72
|
+
|
|
73
|
+
# Report parameters if verbose
|
|
74
|
+
|
|
75
|
+
logging.debug('Source: %s', args.path)
|
|
76
|
+
logging.debug('Pictures: %s', args.picturedir)
|
|
77
|
+
logging.debug('Videos: %s', args.videodir)
|
|
78
|
+
logging.debug('Dry run: %d', args.dryrun)
|
|
79
|
+
|
|
80
|
+
return args
|
|
81
|
+
|
|
82
|
+
################################################################################
|
|
83
|
+
|
|
84
|
+
def get_filetype(filename):
|
|
85
|
+
"""Return the type of a file"""
|
|
86
|
+
|
|
87
|
+
_, ext = os.path.splitext(filename)
|
|
88
|
+
|
|
89
|
+
ext = ext.lower()
|
|
90
|
+
|
|
91
|
+
if ext in IMAGE_EXTENSIONS:
|
|
92
|
+
return FileType.IMAGE
|
|
93
|
+
|
|
94
|
+
if ext in VIDEO_EXTENSIONS:
|
|
95
|
+
return FileType.VIDEO
|
|
96
|
+
|
|
97
|
+
return FileType.UNKNOWN
|
|
98
|
+
|
|
99
|
+
################################################################################
|
|
100
|
+
|
|
101
|
+
def media_sync(args):
|
|
102
|
+
"""Sync photos and videos from args.path to date-structured directory
|
|
103
|
+
trees in args.picturedir and args.videodir.
|
|
104
|
+
Assumes that the source files are in Android naming format:
|
|
105
|
+
(IMG|VID)_YYYYMMDD_*.(jpg|mp4)
|
|
106
|
+
Looks for a destination directory called:
|
|
107
|
+
YYYY/YYYY-MM-DD*/
|
|
108
|
+
If multiple destination directories exist, it uses the first one when the
|
|
109
|
+
names are sorted alphbetically
|
|
110
|
+
If a file with the same name exists in the destination directory it is
|
|
111
|
+
not overwritten"""
|
|
112
|
+
|
|
113
|
+
files_copied = 0
|
|
114
|
+
|
|
115
|
+
filetype_re = re.compile(r'(PANO|IMG|VID)[-_](\d{4})(\d{2})(\d{2})[-_.].*')
|
|
116
|
+
|
|
117
|
+
for sourcefile in [source for source in glob.glob(os.path.join(args.path, '*')) if os.path.isfile(source)]:
|
|
118
|
+
filetype = get_filetype(sourcefile)
|
|
119
|
+
|
|
120
|
+
if filetype == FileType.IMAGE:
|
|
121
|
+
dest_dir = args.picturedir
|
|
122
|
+
elif filetype == FileType.VIDEO:
|
|
123
|
+
dest_dir = args.videodir
|
|
124
|
+
else:
|
|
125
|
+
logging.info('Ignoring %s - unable to determine file type', sourcefile)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
date_match = filetype_re.fullmatch(os.path.basename(sourcefile))
|
|
129
|
+
if not date_match:
|
|
130
|
+
logging.debug('Ignoring %s - unable to extract date from filename', sourcefile)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
year = date_match.group(2)
|
|
134
|
+
month = date_match.group(3)
|
|
135
|
+
day = date_match.group(4)
|
|
136
|
+
|
|
137
|
+
default_dest_dir = f'{dest_dir}/{year}/{year}-{month}-{day}'
|
|
138
|
+
dest_dir_pattern = f'{default_dest_dir}*'
|
|
139
|
+
|
|
140
|
+
dest_dirs = [path for path in glob.glob(dest_dir_pattern) if os.path.isdir(path)]
|
|
141
|
+
|
|
142
|
+
sourcefile_name = os.path.basename(sourcefile)
|
|
143
|
+
|
|
144
|
+
# Search any matching destination directories to see if the file exists
|
|
145
|
+
|
|
146
|
+
if dest_dirs:
|
|
147
|
+
for dest_dir in dest_dirs:
|
|
148
|
+
if os.path.isfile(os.path.join(dest_dir, sourcefile_name)):
|
|
149
|
+
break
|
|
150
|
+
else:
|
|
151
|
+
dest_dir = sorted(dest_dirs)[0]
|
|
152
|
+
else:
|
|
153
|
+
if not args.dryrun:
|
|
154
|
+
os.makedirs(default_dest_dir)
|
|
155
|
+
|
|
156
|
+
dest_dir = default_dest_dir
|
|
157
|
+
|
|
158
|
+
dest_file = os.path.join(dest_dir, sourcefile_name)
|
|
159
|
+
|
|
160
|
+
if os.path.exists(dest_file):
|
|
161
|
+
logging.debug('Destination file %s already exists', dest_file)
|
|
162
|
+
else:
|
|
163
|
+
logging.info('Copying %s to %s', sourcefile, dest_file)
|
|
164
|
+
|
|
165
|
+
if not args.dryrun:
|
|
166
|
+
shutil.copyfile(sourcefile, dest_file)
|
|
167
|
+
|
|
168
|
+
files_copied += 1
|
|
169
|
+
|
|
170
|
+
if files_copied:
|
|
171
|
+
print(f'{files_copied} files copied')
|
|
172
|
+
|
|
173
|
+
################################################################################
|
|
174
|
+
|
|
175
|
+
def localphotosync():
|
|
176
|
+
"""Entry point"""
|
|
177
|
+
try:
|
|
178
|
+
args = parse_command_line()
|
|
179
|
+
|
|
180
|
+
media_sync(args)
|
|
181
|
+
|
|
182
|
+
except KeyboardInterrupt:
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
except BrokenPipeError:
|
|
186
|
+
sys.exit(2)
|
|
187
|
+
|
|
188
|
+
################################################################################
|
|
189
|
+
|
|
190
|
+
if __name__ == '__main__':
|
|
191
|
+
localphotosync()
|
|
@@ -21,21 +21,21 @@ def read_image_hashes(directories):
|
|
|
21
21
|
hashes = defaultdict(list)
|
|
22
22
|
|
|
23
23
|
# Walk each directory tree
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
for directory in directories:
|
|
26
26
|
print(f'Scanning directory tree {directory}')
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
for root, _, files in os.walk(directory):
|
|
29
29
|
print(f'Scanning directory {root}')
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
for file in files:
|
|
32
32
|
filepath = os.path.join(root, file)
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
fileext = os.path.splitext(file)[1]
|
|
35
35
|
|
|
36
36
|
if fileext.lower() not in ('.jbf', '.ini', '.xml', '.ffs_db'):
|
|
37
37
|
# Calculate the hash and store path, dimensions and file size under the hash entry in the hashes table
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
try:
|
|
40
40
|
with Image.open(filepath) as image:
|
|
41
41
|
hash_value = imagehash.average_hash(image, hash_size=12)
|
|
@@ -48,9 +48,9 @@ def read_image_hashes(directories):
|
|
|
48
48
|
|
|
49
49
|
except OSError:
|
|
50
50
|
sys.stderr.write(f'ERROR: Unable to read {filepath} (size={size})\n')
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
# Return the hash table
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
return hashes
|
|
55
55
|
|
|
56
56
|
################################################################################
|
|
@@ -62,11 +62,11 @@ def main():
|
|
|
62
62
|
parser.add_argument('directories', nargs='*', action='store', help='Directories to search')
|
|
63
63
|
|
|
64
64
|
args = parser.parse_args()
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
if not args.directories:
|
|
67
67
|
print('You must be specify at least one directory')
|
|
68
68
|
sys.exit(1)
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
try:
|
|
71
71
|
print('Loading cached data')
|
|
72
72
|
|
|
@@ -78,7 +78,7 @@ def main():
|
|
|
78
78
|
hashes = read_image_hashes(args.directories)
|
|
79
79
|
|
|
80
80
|
# Sort the list of hashes so that we can easily find close matches
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
print('Sorting hashes')
|
|
83
83
|
|
|
84
84
|
hash_values = sorted([str(hashval) for hashval in hashes])
|
|
@@ -70,8 +70,10 @@ def audit(package, version):
|
|
|
70
70
|
def main():
|
|
71
71
|
""" Entry point """
|
|
72
72
|
|
|
73
|
-
parser = argparse.ArgumentParser(
|
|
74
|
-
|
|
73
|
+
parser = argparse.ArgumentParser(
|
|
74
|
+
description='Query api.osv.dev to determine whether Python packagers in a requirments.txt file are subject to known security vulnerabilities')
|
|
75
|
+
parser.add_argument('requirements', nargs='*', type=str, action='store',
|
|
76
|
+
help='The requirements file (if not specified, then the script searches for a requirements.txt file)')
|
|
75
77
|
args = parser.parse_args()
|
|
76
78
|
|
|
77
79
|
requirements = args.requirements or glob.glob('**/requirements.txt', recursive=True)
|
|
@@ -47,13 +47,13 @@ TF_TAG_ENTRY_IGNORE = re.compile(r'^ +".*" += +".*"')
|
|
|
47
47
|
TF_TAG_CHANGE_BLOCK_END = re.compile(r'^ +}$')
|
|
48
48
|
|
|
49
49
|
TF_MISC_REGEX = \
|
|
50
|
-
[
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
]
|
|
50
|
+
[
|
|
51
|
+
{'regex': re.compile(r'(Read complete after) (\d+s|\d+m\d+s)'), 'replace': r'\1 {ELAPSED}'},
|
|
52
|
+
{'regex': re.compile(r'"(.*:.*)"( = ".*")'), 'replace': r'\1\2'},
|
|
53
|
+
{'regex': re.compile(r'"(.*:.*)"( = \[$)'), 'replace': r'\1\2'},
|
|
54
|
+
{'regex': re.compile(r'^last "terraform apply":$'), 'replace': r'last "terraform apply" which may have affected this plan:'},
|
|
55
|
+
{'find': ' ~ ', 'replace': ' * '},
|
|
56
|
+
]
|
|
57
57
|
|
|
58
58
|
TF_IGNORE_LIST = [
|
|
59
59
|
{'start': TF_HAS_CHANGED, 'end': TF_HAS_CHANGED_END},
|
|
@@ -91,8 +91,10 @@ def parse_command_line():
|
|
|
91
91
|
parser.add_argument('--terraform', '-T', action='store_true', help='Clean Terraform plan/apply log files')
|
|
92
92
|
parser.add_argument('--replace', '-R', action='append', default=None, help='Additional regex replacements in the form "REGEX=REPLACEMENT"')
|
|
93
93
|
parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose status')
|
|
94
|
-
parser.add_argument('--minimal', '-m', action='store_true',
|
|
95
|
-
|
|
94
|
+
parser.add_argument('--minimal', '-m', action='store_true',
|
|
95
|
+
help='Remove unnecessary data from the file (e.g. Terraform progress updates (Refreshing..., Reading..., etc.))')
|
|
96
|
+
parser.add_argument('--non-minimal', '-M', action='store_true',
|
|
97
|
+
help='Do not remove unnecessary data from the file (e.g. Terraform progress updates (Refreshing..., Reading..., etc.))')
|
|
96
98
|
parser.add_argument('files', nargs='*', default=None, help='The files to convert (use stdin/stout if no input files are specified)')
|
|
97
99
|
|
|
98
100
|
args = parser.parse_args()
|
|
@@ -144,7 +146,7 @@ def parse_command_line():
|
|
|
144
146
|
for entry in args.replace:
|
|
145
147
|
regex, replace = entry.split('=')
|
|
146
148
|
try:
|
|
147
|
-
args.regex_replace.append({'regex': re.compile(regex), 'replace':replace})
|
|
149
|
+
args.regex_replace.append({'regex': re.compile(regex), 'replace': replace})
|
|
148
150
|
except re.error as exc:
|
|
149
151
|
print(f'ERROR in regular expression {regex}: {exc}')
|
|
150
152
|
sys.exit(1)
|
|
@@ -291,7 +293,7 @@ def cleanfile(args, infile, outfile):
|
|
|
291
293
|
# Write normal output, skipping >1 blank lines and skipping ignore blocks when the pre-ignore
|
|
292
294
|
# count has hit zero.
|
|
293
295
|
|
|
294
|
-
if clean is not None and not (ignore_until and pre_ignore_count==0):
|
|
296
|
+
if clean is not None and not (ignore_until and pre_ignore_count == 0):
|
|
295
297
|
if clean != '' or prev_line != '':
|
|
296
298
|
outfile.write(clean)
|
|
297
299
|
outfile.write('\n')
|
|
@@ -57,7 +57,7 @@ def show_cpu_times(scr, first, w, h, x, y):
|
|
|
57
57
|
scr.addstr(y+2, x, 'IRQ:')
|
|
58
58
|
scr.addstr(y+3, x, 'Soft IRQ:')
|
|
59
59
|
|
|
60
|
-
x+= w//3
|
|
60
|
+
x += w//3
|
|
61
61
|
|
|
62
62
|
scr.addstr(y+1, x, 'Guest:')
|
|
63
63
|
scr.addstr(y+2, x, 'Guest Nice:')
|
|
@@ -320,7 +320,7 @@ def show_temperatures(scr, first, w, h, x, y):
|
|
|
320
320
|
|
|
321
321
|
# Panel title and the functions used to update them
|
|
322
322
|
|
|
323
|
-
BOXES= {
|
|
323
|
+
BOXES = {
|
|
324
324
|
'System Load': show_system_load,
|
|
325
325
|
'Disk Access': show_disk_access,
|
|
326
326
|
'Processes': show_processes,
|
|
@@ -186,7 +186,7 @@ def write(txt=None, newline=True, stream=sys.stdout, indent=0, strip=False, clea
|
|
|
186
186
|
|
|
187
187
|
def error(txt, newline=True, stream=sys.stderr, status=1, prefix=False):
|
|
188
188
|
""" Write an error message to the specified stream (defaulting to
|
|
189
|
-
stderr) and exit with the specified status code (defaulting to 1)
|
|
189
|
+
stderr) and exit with the specified status code (defaulting to 1)
|
|
190
190
|
Prefix the output with 'ERROR:' in red if prefix==True """
|
|
191
191
|
|
|
192
192
|
if prefix:
|
|
@@ -20,6 +20,7 @@ import thingy.dc_util as dc_util
|
|
|
20
20
|
__all__ = ['Dircolors']
|
|
21
21
|
|
|
22
22
|
_CODE_MAP = OrderedDict()
|
|
23
|
+
|
|
23
24
|
def _init_code_map():
|
|
24
25
|
""" mapping between the key name in the .dircolors file and the two letter
|
|
25
26
|
code found in the LS_COLORS environment variable.
|
|
@@ -53,6 +54,7 @@ class Dircolors:
|
|
|
53
54
|
""" Main dircolors class. Contains a database of formats corresponding to file types,
|
|
54
55
|
modes, and extensions. Use the format() method to check a file and color it appropriately.
|
|
55
56
|
"""
|
|
57
|
+
|
|
56
58
|
def __init__(self, load=True):
|
|
57
59
|
""" Initialize a Dircolors object. If load=True (the default), then try
|
|
58
60
|
to load dircolors info from the LS_COLORS environment variable.
|
|
@@ -98,7 +100,7 @@ class Dircolors:
|
|
|
98
100
|
try:
|
|
99
101
|
code, color = item.split('=', 1)
|
|
100
102
|
except ValueError:
|
|
101
|
-
continue
|
|
103
|
+
continue # no key=value, just ignore
|
|
102
104
|
if code.startswith('*.'):
|
|
103
105
|
self._extensions[code[1:]] = color
|
|
104
106
|
else:
|
|
@@ -134,7 +136,7 @@ class Dircolors:
|
|
|
134
136
|
elif isinstance(database, TextIOBase):
|
|
135
137
|
file = database
|
|
136
138
|
else:
|
|
137
|
-
raise ValueError('database must be str or io.TextIOBase, not %s'%type(database))
|
|
139
|
+
raise ValueError('database must be str or io.TextIOBase, not %s' % type(database))
|
|
138
140
|
|
|
139
141
|
try:
|
|
140
142
|
for line in file:
|
|
@@ -147,18 +149,18 @@ class Dircolors:
|
|
|
147
149
|
split = line.split()
|
|
148
150
|
if len(split) != 2:
|
|
149
151
|
if strict:
|
|
150
|
-
raise ValueError('Warning: unable to parse dircolors line "%s"'%line)
|
|
152
|
+
raise ValueError('Warning: unable to parse dircolors line "%s"' % line)
|
|
151
153
|
continue
|
|
152
154
|
|
|
153
155
|
key, val = split
|
|
154
156
|
if key == 'TERM':
|
|
155
|
-
continue
|
|
157
|
+
continue # ignore TERM directives
|
|
156
158
|
elif key in _CODE_MAP:
|
|
157
159
|
self._codes[_CODE_MAP[key]] = val
|
|
158
160
|
elif key.startswith('.'):
|
|
159
161
|
self._extensions[key] = val
|
|
160
162
|
elif strict:
|
|
161
|
-
raise ValueError('Warning: unable to parse dircolors line "%s"'%line)
|
|
163
|
+
raise ValueError('Warning: unable to parse dircolors line "%s"' % line)
|
|
162
164
|
# elif not strict, skip
|
|
163
165
|
|
|
164
166
|
if self._codes or self._extensions:
|
|
@@ -184,14 +186,14 @@ class Dircolors:
|
|
|
184
186
|
# change .xyz to *.xyz
|
|
185
187
|
yield '*' + pair[0], pair[1]
|
|
186
188
|
|
|
187
|
-
return ':'.join('%s=%s'%pair for pair in gen_pairs())
|
|
189
|
+
return ':'.join('%s=%s' % pair for pair in gen_pairs())
|
|
188
190
|
|
|
189
191
|
def _format_code(self, text, code):
|
|
190
192
|
""" format text with an lscolors code. Return text unmodified if code
|
|
191
193
|
isn't found in the database """
|
|
192
194
|
val = self._codes.get(code, None)
|
|
193
195
|
if val:
|
|
194
|
-
return '\033[%sm%s\033[%sm'%(val, text, self._codes.get('rs', '0'))
|
|
196
|
+
return '\033[%sm%s\033[%sm' % (val, text, self._codes.get('rs', '0'))
|
|
195
197
|
return text
|
|
196
198
|
|
|
197
199
|
def _format_ext(self, text, ext):
|
|
@@ -200,7 +202,7 @@ class Dircolors:
|
|
|
200
202
|
text need not actually end in '.ext' """
|
|
201
203
|
val = self._extensions.get(ext, '0')
|
|
202
204
|
if val:
|
|
203
|
-
return '\033[%sm%s\033[%sm'%(val, text, self._codes.get('rs', '0'))
|
|
205
|
+
return '\033[%sm%s\033[%sm' % (val, text, self._codes.get('rs', '0'))
|
|
204
206
|
return text
|
|
205
207
|
|
|
206
208
|
def format_mode(self, text, mode):
|
|
@@ -225,7 +227,7 @@ class Dircolors:
|
|
|
225
227
|
elif isinstance(mode, os.stat_result):
|
|
226
228
|
mode = mode.st_mode
|
|
227
229
|
else:
|
|
228
|
-
raise ValueError('mode must be int or os.stat_result, not %s'%type(mode))
|
|
230
|
+
raise ValueError('mode must be int or os.stat_result, not %s' % type(mode))
|
|
229
231
|
|
|
230
232
|
if mode:
|
|
231
233
|
if stat.S_ISDIR(mode):
|
|
@@ -244,13 +246,13 @@ class Dircolors:
|
|
|
244
246
|
# special file?
|
|
245
247
|
# pylint: disable=bad-whitespace
|
|
246
248
|
special_types = (
|
|
247
|
-
(stat.S_IFLNK, 'ln'),
|
|
248
|
-
(stat.S_IFIFO, 'pi'),
|
|
249
|
-
(stat.S_IFSOCK, 'so'),
|
|
250
|
-
(stat.S_IFBLK, 'bd'),
|
|
251
|
-
(stat.S_IFCHR, 'cd'),
|
|
252
|
-
(stat.S_ISUID, 'su'),
|
|
253
|
-
(stat.S_ISGID, 'sg'),
|
|
249
|
+
(stat.S_IFLNK, 'ln'), # symlink
|
|
250
|
+
(stat.S_IFIFO, 'pi'), # pipe (FIFO)
|
|
251
|
+
(stat.S_IFSOCK, 'so'), # socket
|
|
252
|
+
(stat.S_IFBLK, 'bd'), # block device
|
|
253
|
+
(stat.S_IFCHR, 'cd'), # character device
|
|
254
|
+
(stat.S_ISUID, 'su'), # setuid
|
|
255
|
+
(stat.S_ISGID, 'sg'), # setgid
|
|
254
256
|
)
|
|
255
257
|
|
|
256
258
|
for mask, code in special_types:
|
|
@@ -290,13 +292,13 @@ class Dircolors:
|
|
|
290
292
|
try:
|
|
291
293
|
statbuf = dc_util.stat_at(file, cwd, follow_symlinks)
|
|
292
294
|
except OSError as e:
|
|
293
|
-
return '%s [Error stat-ing: %s]'%(file, e.strerror)
|
|
295
|
+
return '%s [Error stat-ing: %s]' % (file, e.strerror)
|
|
294
296
|
|
|
295
297
|
mode = statbuf.st_mode
|
|
296
298
|
if (not follow_symlinks) and show_target and stat.S_ISLNK(mode):
|
|
297
299
|
target_path = dc_util.readlink_at(file, cwd)
|
|
298
300
|
try:
|
|
299
|
-
dc_util.stat_at(target_path, cwd)
|
|
301
|
+
dc_util.stat_at(target_path, cwd) # check for broken link
|
|
300
302
|
target = self.format(target_path, cwd, False, False)
|
|
301
303
|
except OSError:
|
|
302
304
|
# format as "orphan"
|
|
@@ -148,7 +148,7 @@ class Pane():
|
|
|
148
148
|
|
|
149
149
|
filestat = os.stat(filename, follow_symlinks=False)
|
|
150
150
|
|
|
151
|
-
info = {'name':filename,
|
|
151
|
+
info = {'name': filename,
|
|
152
152
|
'mode': filestat.st_mode,
|
|
153
153
|
'uid': filestat.st_uid,
|
|
154
154
|
'gid': filestat.st_gid,
|
|
@@ -310,7 +310,7 @@ class Pane():
|
|
|
310
310
|
else:
|
|
311
311
|
self.screen.clrtoeol()
|
|
312
312
|
|
|
313
|
-
#if len(filename) < self.width:
|
|
313
|
+
# if len(filename) < self.width:
|
|
314
314
|
# self.screen.addstr(self.file_list_y + ypos, len(filename), ' ' * (self.width - len(filename)), normal_colour)
|
|
315
315
|
|
|
316
316
|
current_dir = path.trimpath(self.current_dir, self.width)
|
|
@@ -541,7 +541,7 @@ class Pane():
|
|
|
541
541
|
|
|
542
542
|
self.height = height
|
|
543
543
|
self.file_list_h = height-1
|
|
544
|
-
self.width = pane_width-1
|
|
544
|
+
self.width = pane_width-1 # TODO: Why '-1'?
|
|
545
545
|
self.screen.resize(height, pane_width)
|
|
546
546
|
self.screen.mvwin(y, x + pane_width*self.index)
|
|
547
547
|
|