manage number of copies and PDF files
This commit is contained in:
parent
ad2051e0e0
commit
b318b9c46a
5 changed files with 94 additions and 17 deletions
12
README.md
12
README.md
|
@ -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
0
archive/.gitkeep
Normal file
8
dist/index.html
vendored
8
dist/index.html
vendored
|
@ -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>
|
||||||
|
|
8
dist/static/js/httprint.js
vendored
8
dist/static/js/httprint.js
vendored
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
83
httprint.py
83
httprint.py
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue