manage number of copies and PDF files

This commit is contained in:
Davide Alberani 2019-08-15 13:34:44 +02:00
parent ad2051e0e0
commit b318b9c46a
5 changed files with 94 additions and 17 deletions

View file

@ -5,6 +5,11 @@ A very simple web interface to upload and print files.
## Install and run ## Install and run
Dependencies:
* Python 3
* **pdfinfo** executable; usually found in the **poppler-utils** package
To install it: To install it:
``` bash ``` bash
wget https://bootstrap.pypa.io/get-pip.py wget https://bootstrap.pypa.io/get-pip.py
@ -21,7 +26,12 @@ Now you can **point your browser to [http://localhost:7777/](http://localhost:77
You can also **run the server in https**, putting in the *ssl* directory two files named *httprint_key.pem* and *httprint_cert.pem* You can also **run the server in https**, putting in the *ssl* directory two files named *httprint_key.pem* and *httprint_cert.pem*
By default the **--print-with-code** argument is true, and the uploaded files are just scheduled for priting. To actually print them, you should supply the generated code, for example: `curl -X POST http://localhost:7777/api/print/1234` By default:
* the **--print-with-code** argument is true, and the uploaded files are just scheduled for priting. To actually print them, you should supply the generated code, for example: `curl -X POST http://localhost:7777/api/print/1234`
* **--max-pages** is set to 10, limiting the number of allowed copies
* **--pdf-only** is true, meaning that only PDF files are allowed
* **--check-pdf-pages** is true, and the number of pages of a PDF are taken into consideration, calculating the maximum number of pages to print
# License and copyright # License and copyright

0
archive/.gitkeep Normal file
View file

8
dist/index.html vendored
View file

@ -11,8 +11,12 @@
<body> <body>
<h1>Upload a file to print</h1> <h1>Upload a file to print</h1>
<input id="upload-file" type="file" /> File to print: <input id="upload-file" type="file" />
<br>
<br>
Number of copies: <input id="copies" type="number" min="1" max="10" value="1" />
<br>
<br>
<button onClick="uploadFile();">print</button> <button onClick="uploadFile();">print</button>
</body> </body>
</html> </html>

View file

@ -1,9 +1,11 @@
function uploadFile() { function uploadFile() {
let photo = document.getElementById("upload-file").files[0]; let photo = document.getElementById("upload-file").files[0];
let copies = document.getElementById('copies').value;
let formData = new FormData(); let formData = new FormData();
formData.append("file", photo); formData.append("file", photo);
var uploadFile = document.getElementById('upload-file'); formData.append("copies", copies);
var uploadField = document.getElementById('upload-file');
fetch("/api/upload", {method: "POST", body: formData}) fetch("/api/upload", {method: "POST", body: formData})
.then(function(response) { .then(function(response) {
return response.json(); return response.json();
@ -26,7 +28,7 @@ function uploadFile() {
layout: 2 layout: 2
}); });
} }
uploadFile.value = null; uploadField.value = null;
}) })
.catch(function(err) { .catch(function(err) {
iziToast.error({ iziToast.error({
@ -35,6 +37,6 @@ function uploadFile() {
position: 'topCenter', position: 'topCenter',
layout: 2 layout: 2
}); });
uploadFile.value = null; uploadField.value = null;
}); });
} }

View file

@ -37,13 +37,16 @@ API_VERSION = '1.0'
QUEUE_DIR = 'queue' QUEUE_DIR = 'queue'
ARCHIVE = True ARCHIVE = True
ARCHIVE_DIR = 'archive' ARCHIVE_DIR = 'archive'
PRINT_CMD = ['lp'] PRINT_CMD = ['lp', '-n', '%(copies)s']
CODE_DIGITS = 4 CODE_DIGITS = 4
MAX_PAGES = 10
PRINT_WITH_CODE = True PRINT_WITH_CODE = True
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
re_pages = re.compile('^Pages:\s+(\d+)$', re.M | re.I)
class HTTPrintBaseException(Exception): class HTTPrintBaseException(Exception):
"""Base class for httprint custom exceptions. """Base class for httprint custom exceptions.
@ -109,26 +112,40 @@ class BaseHandler(tornado.web.RequestHandler):
self.set_status(status) self.set_status(status)
self.write({'error': False, 'message': message}) self.write({'error': False, 'message': message})
def _run(self, cmd): def _run(self, cmd, fname):
p = subprocess.Popen(cmd, close_fds=True) p = subprocess.Popen(cmd, close_fds=True)
p.communicate() p.communicate()
if self.cfg.archive:
if not os.path.isdir(self.cfg.archive_dir):
os.makedirs(self.cfg.archive_dir)
for fn in glob.glob(fname + '*'):
shutil.move(fn, self.cfg.archive_dir)
for fn in glob.glob(fname + '*'):
try:
os.unlink(fn)
except Exception:
pass
def run_subprocess(self, cmd): def run_subprocess(self, cmd, fname):
"""Execute the given action. """Execute the given action.
:param cmd: the command to be run with its command line arguments :param cmd: the command to be run with its command line arguments
:type cmd: list :type cmd: list
""" """
p = mp.Process(target=self._run, args=(cmd,)) p = mp.Process(target=self._run, args=(cmd, fname))
p.start() p.start()
def print_file(self, fname): def print_file(self, fname):
cmd = PRINT_CMD + [fname] copies = 1
self.run_subprocess(cmd) try:
if self.cfg.archive: with open(fname + '.copies', 'r') as fd:
if not os.path.isdir(self.cfg.archive_dir): copies = int(fd.read())
os.makedirs(self.cfg.archive_dir) if copies < 1:
shutil.move(fname, self.cfg.archive_dir) copies = 1
except Exception:
pass
cmd = [x % {'copies': copies} for x in PRINT_CMD] + [fname]
self.run_subprocess(cmd, fname)
class PrintHandler(BaseHandler): class PrintHandler(BaseHandler):
@ -138,7 +155,8 @@ class PrintHandler(BaseHandler):
if not code: if not code:
self.build_error("empty code") self.build_error("empty code")
return return
files = glob.glob(self.cfg.queue_dir + '/%s-*' % code) files = [x for x in sorted(glob.glob(self.cfg.queue_dir + '/%s-*' % code))
if not x.endswith('.info') and not x.endswith('.pages')]
if not files: if not files:
self.build_error("no matching files") self.build_error("no matching files")
return return
@ -172,6 +190,16 @@ class UploadHandler(BaseHandler):
if not self.request.files.get('file'): if not self.request.files.get('file'):
self.build_error("no file uploaded") self.build_error("no file uploaded")
return return
copies = 1
try:
copies = int(self.get_argument('copies'))
if copies < 1:
copies = 1
except Exception:
pass
if copies > self.cfg.max_pages:
self.build_error('you have asked too many copies')
return
fileinfo = self.request.files['file'][0] fileinfo = self.request.files['file'][0]
webFname = fileinfo['filename'] webFname = fileinfo['filename']
extension = '' extension = ''
@ -195,8 +223,38 @@ class UploadHandler(BaseHandler):
with open(pname + '.info', 'w') as fd: with open(pname + '.info', 'w') as fd:
fd.write('original file name: %s\n' % webFname) fd.write('original file name: %s\n' % webFname)
fd.write('uploaded on: %s\n' % now) fd.write('uploaded on: %s\n' % now)
fd.write('copies: %d\n' % copies)
except Exception: except Exception:
pass pass
try:
with open(pname + '.copies', 'w') as fd:
fd.write('%d' % copies)
except Exception:
pass
failure = False
if self.cfg.check_pdf_pages or self.cfg.pdf_only:
try:
p = subprocess.Popen(['pdfinfo', pname], stdout=subprocess.PIPE)
out, _ = p.communicate()
if p.returncode != 0 and self.cfg.pdf_only:
self.build_error('the uploaded file does not seem to be a PDF')
failure = True
out = out.decode('utf-8', errors='ignore')
pages = int(re_pages.findall(out)[0])
if pages * copies > self.cfg.max_pages and self.cfg.check_pdf_pages:
self.build_error('too many pages to print (%d)' % (pages * copies))
failure = True
except Exception:
self.build_error('unable to get PDF information')
failure = True
pass
if failure:
for fn in glob.glob(pname + '*'):
try:
os.unlink(fn)
except Exception:
pass
return
if self.cfg.print_with_code: if self.cfg.print_with_code:
self.build_success("go to the printer and enter this code: %s" % code) self.build_success("go to the printer and enter this code: %s" % code)
else: else:
@ -225,10 +283,13 @@ def serve():
define('ssl_key', default=os.path.join(os.path.dirname(__file__), 'ssl', 'httprint_key.pem'), define('ssl_key', default=os.path.join(os.path.dirname(__file__), 'ssl', 'httprint_key.pem'),
help='specify the SSL private key to use for secure connections') help='specify the SSL private key to use for secure connections')
define('code-digits', default=CODE_DIGITS, help='number of digits of the code', type=int) define('code-digits', default=CODE_DIGITS, help='number of digits of the code', type=int)
define('max-pages', default=MAX_PAGES, help='maximum number of pages to print', type=int)
define('queue-dir', default=QUEUE_DIR, help='directory to store files before they are printed', type=str) define('queue-dir', default=QUEUE_DIR, help='directory to store files before they are printed', type=str)
define('archive', default=True, help='archive printed files', type=bool) define('archive', default=True, help='archive printed files', type=bool)
define('archive-dir', default=ARCHIVE_DIR, help='directory to archive printed files', type=str) define('archive-dir', default=ARCHIVE_DIR, help='directory to archive printed files', type=str)
define('print-with-code', default=True, help='a code must be entered for printing', type=bool) define('print-with-code', default=True, help='a code must be entered for printing', type=bool)
define('pdf-only', default=True, help='only print PDF files', type=bool)
define('check-pdf-pages', default=True, help='check that the number of pages of PDF files do not exeed --max-pages', type=bool)
define('debug', default=False, help='run in debug mode', type=bool) define('debug', default=False, help='run in debug mode', type=bool)
tornado.options.parse_command_line() tornado.options.parse_command_line()