1
0
mirror of https://github.com/sashacmc/photo-importer.git synced 2025-02-22 18:42:16 +02:00

Merge pull request #9 from sashacmc/1.2.0

This commit is contained in:
Alexander 2022-11-29 23:45:18 +01:00 committed by GitHub
commit 32b832dcbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 277 additions and 181 deletions

View File

@ -9,7 +9,7 @@ Command line tools for photo importing/renaming/rotating
* Media files scan
* Time when picture was taken detection (by EXIF, by file name, by file attributes)
* Media files moving/copying to configurable hierarchy
* Lossless rotations (via exiftran)
* Lossless rotations (via exiftran or jpegtran)
# Photo Importer Server
Standalone web server for fast media import for headless computer
@ -18,31 +18,43 @@ Standalone web server for fast media import for headless computer
* Storages mount/unmount (via pmount)
* The same as photo-importer but without console
## Installation
# Installation
### Requirements:
* Python 3.3+
* Debian based Linux (Other Linux versions not officially supported, but might work)
### Supported OS:
* Debian based Linux (other Linux versions not officially supported, but might work)
* Windows 7 and above
* MacOS X and above (with brew installed, only console version)
### Dependencies:
* PyExifTool (pip3 install PyExifTool)
* python3-progressbar
* python3-psutil
* exiftran or jpegtran
* pmount (only for server)
* pypiwin32 (only for windows)
* [PyExifTool](https://pypi.org/project/PyExifTool/)
* [progressbar](https://pypi.org/project/progressbar/)
* [psutil](https://pypi.org/project/psutil/)
* [exiftran](https://linux.die.net/man/1/exiftran) or [jpegtran](https://linux.die.net/man/1/jpegtran)
* [pmount](https://linux.die.net/man/1/pmount) (only for server)
* [pypiwin32](https://pypi.org/project/pypiwin32/) (only for windows)
### Installation Options:
#### Installing via PyPi
```bash
sudo apt install exiftran exiftool pmount pip
sudo pip install photo-importer
```
#### Installing as debian package
```bash
debuild -b
sudo dpkg -i ../photo-importer_1.0.1_all.deb
sudo dpkg -i ../photo-importer_1.2.0_all.deb
```
#### Installing via setup.py
```bash
sudo apt install exiftran exiftool pmount pip
sudo pip install PyExifTool progressbar psutil
sudo python3 ./setup.py install
```
@ -56,9 +68,9 @@ https://exiftool.org/
Download and extract jpegtran to photo_importer folder
http://sylvana.net/jpegcrop/jpegtran/
Install python dependencies
Install with python dependencies
```bash
python -m pip install progressbar psutil pyexiftool pypiwin32
python -m pip install pypiwin32 photo-importer
```
## Usage

13
debian/changelog vendored
View File

@ -1,3 +1,16 @@
photo-importer (1.2.0) stable; urgency=medium
* Add PyPi install support
* Update to new PyExifTool
* Reformat with black
* Update modules import
* Add importer test
* Improve fileprop tests
* Remove legacy modules __main__
* MacOS support verified
-- Alexander Bushnev <Alexander@Bushnev.pro> Tue, 29 Nov 2022 09:03:24 +0100
photo-importer (1.1.2) stable; urgency=medium
* Update setup scripts

View File

@ -35,7 +35,7 @@ class Config(object):
'out_path': '/mnt/multimedia/NEW/',
'in_path': '',
'log_file': 'photo-importer-server.log',
}
},
}
def __init__(self, filename=None, create=False):
@ -44,7 +44,11 @@ class Config(object):
self.__config = configparser.ConfigParser()
self.__config.read_dict(self.DEFAULTS)
self.__config.read([filename, ])
self.__config.read(
[
filename,
]
)
if create:
self.__create_if_not_exists()
@ -59,6 +63,9 @@ class Config(object):
def __getitem__(self, sect):
return self.__config[sect]
def set(self, sect, name, val):
self.__config[sect][name] = val
if __name__ == "__main__":
Config(create=True)

View File

@ -17,17 +17,27 @@ GARBAGE = 4
class FileProp(object):
DATE_REX = [
(re.compile('\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}'),
'%Y-%m-%d_%H-%M-%S'),
(re.compile('\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}'),
'%Y-%m-%d-%H-%M-%S'),
(re.compile('\d{4}-\d{2}-\d{2}T\d{2}.\d{2}.\d{2}'),
'%Y-%m-%dT%H.%M.%S'),
(re.compile('\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'),
'%Y-%m-%dT%H:%M:%S'),
(re.compile('\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}'),
'%Y_%m_%d_%H_%M_%S'),
DATE_REGEX = [
(
re.compile('\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}'),
'%Y-%m-%d_%H-%M-%S',
),
(
re.compile('\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}'),
'%Y-%m-%d-%H-%M-%S',
),
(
re.compile('\d{4}-\d{2}-\d{2}T\d{2}.\d{2}.\d{2}'),
'%Y-%m-%dT%H.%M.%S',
),
(
re.compile('\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'),
'%Y-%m-%dT%H:%M:%S',
),
(
re.compile('\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}'),
'%Y_%m_%d_%H_%M_%S',
),
(re.compile('\d{8}_\d{6}'), '%Y%m%d_%H%M%S'),
(re.compile('\d{14}'), '%Y%m%d%H%M%S'),
(re.compile('\d{8}'), '%Y%m%d'),
@ -55,14 +65,17 @@ class FileProp(object):
'QuickTime:MediaCreateDate',
'PDF:CreateDate',
'XMP:CreateDate',
'EXIF:CreateDate',
]
def __init__(self, config):
self.__config = config
self.__prepare_ext_to_type()
self.__out_list = set()
self.__exiftool = exiftool.ExifTool()
self.__exiftool.start()
self.__exiftool = exiftool.ExifToolHelper()
def __del__(self):
self.__exiftool.terminate()
def __prepare_ext_to_type(self):
self.EXT_TO_TYPE = {}
@ -101,7 +114,7 @@ class FileProp(object):
return None
def __time_by_name(self, fname):
for exp, fs in self.DATE_REX:
for exp, fs in self.DATE_REGEX:
mat = exp.findall(fname)
if len(mat):
try:
@ -116,7 +129,7 @@ class FileProp(object):
def __time_by_exif(self, fullname):
try:
metadata = self.__exiftool.get_metadata(fullname)
metadata = self.__exiftool.get_metadata(fullname)[0]
for tag in self.DATE_TAGS:
if tag in metadata:
md = metadata[tag]
@ -125,8 +138,10 @@ class FileProp(object):
md = md[0:pos]
return datetime.datetime.strptime(md, '%Y:%m:%d %H:%M:%S')
logging.warning('time by exif (%s) not found tags count: %s' %
(fullname, len(metadata)))
logging.warning(
'time by exif (%s) not found tags count: %s'
% (fullname, len(metadata))
)
for tag, val in metadata.items():
logging.debug('%s: %s' % (tag, val))
return None
@ -136,7 +151,8 @@ class FileProp(object):
def __time_by_attr(self, fullname):
try:
return datetime.datetime.fromtimestamp(
time.mktime(time.localtime(os.stat(fullname)[stat.ST_MTIME])))
time.mktime(time.localtime(os.stat(fullname)[stat.ST_MTIME]))
)
except (FileNotFoundError, KeyError) as ex:
logging.warning('time by attr (%s) exception: %s' % (fullname, ex))
@ -161,13 +177,12 @@ class FileProp(object):
ftime = self.__time(fullname, fname, tp)
if ftime:
out_name = ftime.strftime(
self.__config['main']['out_time_format'])
out_name = ftime.strftime(self.__config['main']['out_time_format'])
else:
out_name = None
if out_name:
ok = fname[0:len(out_name)] == out_name
ok = fname[0 : len(out_name)] == out_name
else:
ok = False
@ -207,11 +222,13 @@ class FilePropRes(object):
path = self.__path
return self.__prop_ptr._out_name_full(
path, self.__out_name, self.__ext)
path, self.__out_name, self.__ext
)
if __name__ == '__main__':
import sys
sys.path.insert(0, os.path.abspath('..'))
from photo_importer import log

View File

@ -1,20 +1,19 @@
#!/usr/bin/python3
import os
import sys
import unittest
import datetime
sys.path.insert(0, os.path.abspath('..'))
from photo_importer import config # noqa
from photo_importer import fileprop # noqa
from . import config
from . import fileprop
class TestFileProp(unittest.TestCase):
def setUp(self):
self.conf = config.Config()
self.fp = fileprop.FileProp(self.conf)
self.conf.set('main', 'time_src_image', 'name')
self.conf.set('main', 'time_src_video', 'name')
self.conf.set('main', 'time_src_audio', 'name')
# photo
def test_camera_photo(self):
@ -145,7 +144,3 @@ class TestFileProp(unittest.TestCase):
self.assertEqual(fp.type(), fileprop.GARBAGE)
self.assertEqual(fp.time(), None)
self.assertEqual(fp.ok(), False)
if __name__ == '__main__':
unittest.main()

View File

@ -1,17 +1,13 @@
#!/usr/bin/python3
import os
import sys
import logging
import threading
sys.path.insert(0, os.path.abspath('..'))
from photo_importer import log # noqa
from photo_importer import mover # noqa
from photo_importer import config # noqa
from photo_importer import rotator # noqa
from photo_importer import fileprop # noqa
from . import log
from . import mover
from . import rotator
from . import fileprop
class Importer(threading.Thread):
@ -28,8 +24,9 @@ class Importer(threading.Thread):
def run(self):
logging.info(
'Start: %s -> %s (dryrun: %s)' %
(self.__input_path, self.__output_path, self.__dryrun))
'Start: %s -> %s (dryrun: %s)'
% (self.__input_path, self.__output_path, self.__dryrun)
)
filenames, dirs = self.__scan_files(self.__input_path)
@ -48,7 +45,8 @@ class Importer(threading.Thread):
res_dir = []
res = []
for root, dirs, files in os.walk(
input_path, onerror=self.__on_walk_error):
input_path, onerror=self.__on_walk_error
):
for fname in files:
res.append(os.path.join(root, fname))
@ -72,7 +70,8 @@ class Importer(threading.Thread):
self.__input_path,
self.__output_path,
filenames,
self.__dryrun)
self.__dryrun,
)
self.__stat['stage'] = 'move'
res = self.__mov.run()
@ -88,10 +87,7 @@ class Importer(threading.Thread):
def __rotate_files(self, filenames):
logging.info('Rotating')
self.__rot = rotator.Rotator(
self.__config,
filenames,
self.__dryrun)
self.__rot = rotator.Rotator(self.__config, filenames, self.__dryrun)
self.__stat['stage'] = 'rotate'
self.__rot.run()
@ -117,15 +113,3 @@ class Importer(threading.Thread):
def log_text(self):
return self.__log.get_text()
if __name__ == '__main__':
import sys
log.initLogger()
imp = Importer(config.Config(), sys.argv[1], sys.argv[2], False)
imp.start()
imp.join()
print(imp.status())

67
photo_importer/importer_test.py Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/python3
import os
import unittest
import tempfile
from . import config
from . import importer
class TestImporter(unittest.TestCase):
def test_importer(self):
with tempfile.TemporaryDirectory() as tmpdirname:
cfg = config.Config()
cfg.set('main', 'move_mode', '0')
imp = importer.Importer(
cfg,
os.path.join(os.path.dirname(__file__), 'test_data'),
tmpdirname,
False,
)
imp.start()
imp.join()
self.assertEqual(
imp.status(),
{
'stage': 'done',
'total': 2,
'move': {
'total': 2,
'moved': 0,
'copied': 2,
'removed': 0,
'skipped': 0,
'processed': 2,
'errors': 0,
},
'rotate': {
'total': 2,
'processed': 2,
'good': 2,
'errors': 0,
},
},
)
files = []
for path, cd, fs in os.walk(tmpdirname):
for f in fs:
print(os.path.join(path, f))
files.append(os.path.join(path, f))
files.sort()
self.assertEqual(len(files), 2)
self.assertEqual(
files[0],
os.path.join(
tmpdirname, 'Foto/2021/2021-12-19/2021-12-19_13-11-36.jpeg'
),
)
self.assertEqual(
files[1],
os.path.join(
tmpdirname, 'Foto/2022/2022-11-21/2022-11-21_00-42-07.JPG'
),
)

View File

@ -5,7 +5,7 @@ import shutil
import logging
import subprocess
from photo_importer import fileprop
from . import fileprop
class Mover(object):
@ -71,7 +71,9 @@ class Mover(object):
os.path.join(
self.__output_path,
self.__config['main'][self.OUT_SUBDIR_CFG[prop.type()]],
self.__config['main']['out_date_format']))
self.__config['main']['out_date_format'],
)
)
if not os.path.isdir(path):
if not self.__dryrun:
@ -120,9 +122,8 @@ class Mover(object):
return True
with subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE) as proc:
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
) as proc:
proc.wait()
info = proc.stdout.read().strip()
if info:
@ -131,8 +132,9 @@ class Mover(object):
if err:
logging.error(err.decode("utf-8"))
elif proc.returncode != 0:
logging.error('%s failed with code %i' %
(args[0], proc.returncode))
logging.error(
'%s failed with code %i' % (args[0], proc.returncode)
)
return proc.returncode == 0
def status(self):

View File

@ -7,7 +7,6 @@ import tempfile
import subprocess
import concurrent.futures
from photo_importer import config
JPEGTRAN_COMMAND = {
0: None,
@ -18,7 +17,7 @@ JPEGTRAN_COMMAND = {
5: '-transpose',
6: '-rotate 90',
7: '-transverse',
8: '-rotate 270'
8: '-rotate 270',
}
ORIENTATION_TAG = 'EXIF:Orientation'
@ -39,16 +38,15 @@ class Rotator(object):
processor = self.__process_exiftran
self.__exiftool = None
if int(self.__config['main']['use_jpegtran']):
self.__exiftool = exiftool.ExifTool()
self.__exiftool.start()
self.__exiftool = exiftool.ExifToolHelper()
processor = self.__process_jpegtran
tc = 1
with concurrent.futures.ThreadPoolExecutor(max_workers=tc) as executor:
futures = {
executor.submit(processor, fn):
fn for fn in self.__filenames}
executor.submit(processor, fn): fn for fn in self.__filenames
}
for future in concurrent.futures.as_completed(futures):
self.__processed += 1
@ -69,24 +67,24 @@ class Rotator(object):
if self.__dryrun:
return True
p = subprocess.Popen(
error = ''
with subprocess.Popen(
cmd,
shell=True,
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stderr
stderr=subprocess.PIPE,
) as p:
while True:
line = p.stderr.readline()
if not line:
break
error = ''
while True:
line = p.readline()
if not line:
break
if line.startswith('processing '):
ok = True
else:
ok = False
error += line
if line.startswith('processing '):
ok = True
else:
ok = False
error += line
if error != '':
logging.error('exiftran (%s) error: %s' % (filename, error))
@ -102,8 +100,9 @@ class Rotator(object):
if orientation_cmd is None:
return True
logging.debug('rotate: jpegtran %s %s' %
(orientation_cmd, filename))
logging.debug(
'rotate: jpegtran %s %s' % (orientation_cmd, filename)
)
if self.__dryrun:
return True
@ -111,20 +110,25 @@ class Rotator(object):
handle, tmpfile = tempfile.mkstemp(dir=os.path.dirname(filename))
os.close(handle)
cmd = 'jpegtran -copy all -outfile %s %s %s' % \
(tmpfile, orientation_cmd, filename)
cmd = 'jpegtran -copy all -outfile %s %s %s' % (
tmpfile,
orientation_cmd,
filename,
)
p = subprocess.Popen(
with subprocess.Popen(
cmd,
shell=True,
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stderr
line = p.readline()
if line:
logging.error('jpegtran (%s) failed: %s' % (filename, line))
return False
stderr=subprocess.PIPE,
) as p:
line = p.stderr.readline()
if line:
logging.error(
'jpegtran (%s) failed: %s' % (filename, line)
)
return False
self.__clear_orientation_tag(tmpfile)
@ -137,18 +141,17 @@ class Rotator(object):
return False
def __get_orientation_cmd(self, fullname):
orientation = self.__exiftool.get_tag(ORIENTATION_TAG, fullname)
if orientation is not None and \
0 <= orientation and orientation < len(JPEGTRAN_COMMAND):
tags = self.__exiftool.get_tags(fullname, ORIENTATION_TAG)
if ORIENTATION_TAG not in tags:
return None
orientation = tags[ORIENTATION_TAG]
if 0 <= orientation and orientation < len(JPEGTRAN_COMMAND):
return JPEGTRAN_COMMAND[orientation]
else:
return None
def __clear_orientation_tag(self, fullname):
res = self.__exiftool.set_tags(
{ORIENTATION_TAG: 1}, fullname).decode('utf-8')
if not exiftool.check_ok(res):
raise SystemError('exiftool error: ' + exiftool.format_error(res))
self.__exiftool.set_tags(fullname, {ORIENTATION_TAG: 1})
try:
os.remove(fullname + '_original')
except Exception:
@ -159,13 +162,5 @@ class Rotator(object):
'total': len(self.__filenames),
'processed': self.__processed,
'good': self.__good,
'errors': self.__errors}
if __name__ == '__main__':
import sys
rot = Rotator(config.Config(), sys.argv[1:], False)
rot.run()
print(rot.status())
'errors': self.__errors,
}

View File

@ -1,17 +1,13 @@
#!/usr/bin/python3
import os
import sys
import logging
import argparse
import threading
import progressbar
sys.path.insert(0, os.path.abspath('..'))
from photo_importer import log # noqa
from photo_importer import config # noqa
from photo_importer import importer # noqa
from . import log
from . import config
from . import importer
class ProgressBar(threading.Thread):
@ -31,10 +27,15 @@ class ProgressBar(threading.Thread):
self.__pbar = progressbar.ProgressBar(
maxval=count,
widgets=[
name, ' ',
progressbar.Percentage(), ' ',
progressbar.Bar(), ' ',
progressbar.ETA()]).start()
name,
' ',
progressbar.Percentage(),
' ',
progressbar.Bar(),
' ',
progressbar.ETA(),
],
).start()
def run(self):
stage = ''
@ -58,8 +59,9 @@ class ProgressBar(threading.Thread):
self.__pbar.finish()
break
if (stage == 'move' or stage == 'rotate') and \
self.__pbar is not None:
if (
stage == 'move' or stage == 'rotate'
) and self.__pbar is not None:
self.__pbar.update(stat[stage]['processed'])
@ -80,11 +82,7 @@ def main():
log.initLogger(args.logfile)
imp = importer.Importer(
cfg,
args.in_path,
args.out_path,
args.dryrun)
imp = importer.Importer(cfg, args.in_path, args.out_path, args.dryrun)
pbar = ProgressBar(imp)
imp.start()

View File

@ -2,7 +2,6 @@
import os
import re
import sys
import glob
import json
import psutil
@ -12,11 +11,9 @@ import argparse
import http.server
from http import HTTPStatus
sys.path.insert(0, os.path.abspath('..'))
from photo_importer import log # noqa
from photo_importer import config # noqa
from photo_importer import importer # noqa
from . import log
from . import config
from . import importer
FIXED_IN_PATH_NAME = 'none'
@ -32,7 +29,6 @@ class HTTPError(Exception):
class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
def __ok_response(self, result):
self.send_response(200)
self.send_header('Content-type', 'application/json')
@ -49,11 +45,13 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
self.send_error(code, explain=str(err))
def __get_mounted_list(self):
return {os.path.basename(dp.device): (dp.device, dp.mountpoint)
for dp in psutil.disk_partitions()}
return {
os.path.basename(dp.device): (dp.device, dp.mountpoint)
for dp in psutil.disk_partitions()
}
def __bytes_to_gbytes(self, b):
return round(b / 1024. / 1024. / 1024., 2)
return round(b / 1024.0 / 1024.0 / 1024.0, 2)
def __get_removable_devices_posix(self):
mount_list = self.__get_mounted_list()
@ -73,13 +71,13 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
res[pdev] = {
'dev_path': mount_list[pdev][0],
'mount_path': mount_list[pdev][1],
'read_only': read_only
'read_only': read_only,
}
else:
res[pdev] = {
'dev_path': '/dev/' + pdev,
'mount_path': '',
'read_only': read_only
'read_only': read_only,
}
return res
@ -96,7 +94,7 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
res[dev_name] = {
'dev_path': dev_name,
'mount_path': d,
'read_only': not os.access(d, os.W_OK)
'read_only': not os.access(d, os.W_OK),
}
return res
@ -113,8 +111,9 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
res[FIXED_IN_PATH_NAME] = {
'dev_path': FIXED_IN_PATH_NAME,
'mount_path': self.server.fixed_in_path(),
'read_only':
not os.access(self.server.fixed_in_path(), os.W_OK)
'read_only': not os.access(
self.server.fixed_in_path(), os.W_OK
),
}
return res
@ -138,13 +137,15 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
r['progress'] = 0
r['read_only'] = info['read_only']
r['allow_start'] = not (
info['read_only'] and self.server.move_mode())
info['read_only'] and self.server.move_mode()
)
if r['path']:
stat = self.server.import_status(r['path'])
du = psutil.disk_usage(r['path'])
if dev == FIXED_IN_PATH_NAME:
r['size'] = self.__bytes_to_gbytes(
self.__folder_size(r['path']))
self.__folder_size(r['path'])
)
else:
r['size'] = self.__bytes_to_gbytes(du.total)
r['usage'] = du.percent
@ -152,12 +153,13 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
stage = stat['stage']
r['state'] = stage
if stage == 'move' or stage == 'rotate':
r['progress'] = \
round(100. *
stat[stage]['processed'] / stat['total'])
r['progress'] = round(
100.0 * stat[stage]['processed'] / stat['total']
)
elif stage == 'done':
cerr = stat['move']['errors'] + \
stat['rotate']['errors']
cerr = (
stat['move']['errors'] + stat['rotate']['errors']
)
if cerr != 0:
r['state'] = 'error'
r['total'] = cerr
@ -178,8 +180,7 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
raise HTTPError(HTTPStatus.BAD_REQUEST, 'empty "d" param')
dev_list = self.__get_removable_devices()
if dev not in dev_list:
raise HTTPError(HTTPStatus.BAD_REQUEST,
'wrong device: %s' % dev)
raise HTTPError(HTTPStatus.BAD_REQUEST, 'wrong device: %s' % dev)
device = dev_list[dev]
if device['mount_path']:
self.server.import_done(device['mount_path'])
@ -216,8 +217,9 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
elif action == 'umount':
result = self.__mount_umount(dev)
else:
raise HTTPError(HTTPStatus.BAD_REQUEST,
'unknown action %s' % action)
raise HTTPError(
HTTPStatus.BAD_REQUEST, 'unknown action %s' % action
)
self.__ok_response(result)
@ -256,8 +258,9 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
result = self.__import_get_log(in_path)
self.__text_response(result)
else:
raise HTTPError(HTTPStatus.BAD_REQUEST,
'unknown action %s' % action)
raise HTTPError(
HTTPStatus.BAD_REQUEST, 'unknown action %s' % action
)
def __sysinfo_request(self, params):
try:
@ -279,7 +282,8 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
if (path[0]) == '/':
path = path[1:]
fname = os.path.normpath(
os.path.join(self.server.web_path(), path))
os.path.join(self.server.web_path(), path)
)
if not fname.startswith(self.server.web_path()):
logging.warning('incorrect path: ' + path)
raise HTTPError(HTTPStatus.NOT_FOUND, path)
@ -387,10 +391,8 @@ class PhotoImporterServer(http.server.HTTPServer):
logging.info('import_start: %s', in_path)
self.__importers[in_path] = importer.Importer(
self.__cfg,
in_path,
out_path,
False)
self.__cfg, in_path, out_path, False
)
self.__importers[in_path].start()

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -4,7 +4,9 @@ import os
def get_long_description():
this_directory = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
with open(
os.path.join(this_directory, 'README.md'), encoding='utf-8'
) as f:
long_description = f.read()
return long_description
@ -12,7 +14,7 @@ def get_long_description():
setup(
name='photo-importer',
version='1.1.2',
version='1.2.0',
description='Photo importer tool',
author='Alexander Bushnev',
author_email='Alexander@Bushnev.pro',
@ -32,6 +34,7 @@ setup(
'Development Status :: 5 - Production/Stable',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Operating System :: MacOS',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
@ -40,6 +43,7 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Topic :: Scientific/Engineering :: Image Processing',
'Topic :: Multimedia :: Video',
'Topic :: Utilities',