httprint.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """httprint - print files via web
  4. Copyright 2019 Davide Alberani <da@mimante.net>
  5. Licensed under the Apache License, Version 2.0 (the "License");
  6. you may not use this file except in compliance with the License.
  7. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. """
  14. import os
  15. import time
  16. import random
  17. import asyncio
  18. import logging
  19. import subprocess
  20. import multiprocessing as mp
  21. from tornado.ioloop import IOLoop
  22. import tornado.httpserver
  23. import tornado.options
  24. from tornado.options import define, options
  25. import tornado.web
  26. from tornado import gen, escape
  27. API_VERSION = '1.0'
  28. UPLOAD_PATH = 'uploads'
  29. PRINT_CMD = ['lp']
  30. PROCESS_TIMEOUT = 60
  31. ENCODING = 'utf-8'
  32. logger = logging.getLogger()
  33. logger.setLevel(logging.INFO)
  34. class HTTPrintBaseException(Exception):
  35. """Base class for httprint custom exceptions.
  36. :param message: text message
  37. :type message: str
  38. :param status: numeric http status code
  39. :type status: int"""
  40. def __init__(self, message, status=400):
  41. super(HTTPrintBaseException, self).__init__(message)
  42. self.message = message
  43. self.status = status
  44. class BaseHandler(tornado.web.RequestHandler):
  45. """Base class for request handlers."""
  46. # A property to access the first value of each argument.
  47. arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
  48. for k, v in self.request.arguments.items()]))
  49. @property
  50. def clean_body(self):
  51. """Return a clean dictionary from a JSON body, suitable for a query on MongoDB.
  52. :returns: a clean copy of the body arguments
  53. :rtype: dict"""
  54. return escape.json_decode(self.request.body or '{}')
  55. def write_error(self, status_code, **kwargs):
  56. """Default error handler."""
  57. if isinstance(kwargs.get('exc_info', (None, None))[1], HTTPrintBaseException):
  58. exc = kwargs['exc_info'][1]
  59. status_code = exc.status
  60. message = exc.message
  61. else:
  62. message = 'internal error'
  63. self.build_error(message, status=status_code)
  64. def initialize(self, **kwargs):
  65. """Add every passed (key, value) as attributes of the instance."""
  66. for key, value in kwargs.items():
  67. setattr(self, key, value)
  68. def build_error(self, message='', status=400):
  69. """Build and write an error message.
  70. :param message: textual message
  71. :type message: str
  72. :param status: HTTP status code
  73. :type status: int
  74. """
  75. self.set_status(status)
  76. self.write({'error': True, 'message': message})
  77. def build_success(self, message='', status=200):
  78. """Build and write a success message.
  79. :param message: textual message
  80. :type message: str
  81. :param status: HTTP status code
  82. :type status: int
  83. """
  84. self.set_status(status)
  85. self.write({'error': False, 'message': message})
  86. def _run(self, cmd):
  87. p = subprocess.Popen(cmd, close_fds=True)
  88. p.communicate()
  89. def run_subprocess(self, cmd):
  90. """Execute the given action.
  91. :param cmd: the command to be run with its command line arguments
  92. :type cmd: list
  93. """
  94. p = mp.Process(target=self._run, args=(cmd,))
  95. p.start()
  96. class UploadHandler(BaseHandler):
  97. """Reset schedules handler."""
  98. @gen.coroutine
  99. def post(self):
  100. if not self.request.files.get('file'):
  101. self.build_error("no file uploaded")
  102. return
  103. fileinfo = self.request.files['file'][0]
  104. webFname = fileinfo['filename']
  105. extension = ''
  106. try:
  107. extension = os.path.splitext(webFname)[1]
  108. except Exception:
  109. pass
  110. if not os.path.isdir(UPLOAD_PATH):
  111. os.makedirs(UPLOAD_PATH)
  112. fname = '%s-%s%s' % (
  113. time.strftime('%Y%m%d%H%M%S'),
  114. '%04d' % random.randint(0, 9999),
  115. extension)
  116. pname = os.path.join(UPLOAD_PATH, fname)
  117. try:
  118. with open(pname, 'wb') as fd:
  119. fd.write(fileinfo['body'])
  120. except Exception as e:
  121. self.build_error("error writing file %s: %s" % (pname, e))
  122. return
  123. try:
  124. with open(pname + '.info', 'w') as fd:
  125. fd.write('originale file name: %s\n' % webFname)
  126. except Exception:
  127. pass
  128. cmd = PRINT_CMD + [pname]
  129. self.run_subprocess(cmd)
  130. self.build_success("file sent to printer")
  131. class TemplateHandler(BaseHandler):
  132. """Handler for the template files in the / path."""
  133. @gen.coroutine
  134. def get(self, *args, **kwargs):
  135. """Get a template file."""
  136. page = 'index.html'
  137. if args and args[0]:
  138. page = args[0].strip('/')
  139. arguments = self.arguments
  140. self.render(page, **arguments)
  141. def serve():
  142. """Read configuration and start the server."""
  143. define('port', default=7777, help='run on the given port', type=int)
  144. define('address', default='', help='bind the server at the given address', type=str)
  145. define('ssl_cert', default=os.path.join(os.path.dirname(__file__), 'ssl', 'httprint_cert.pem'),
  146. help='specify the SSL certificate to use for secure connections')
  147. define('ssl_key', default=os.path.join(os.path.dirname(__file__), 'ssl', 'httprint_key.pem'),
  148. help='specify the SSL private key to use for secure connections')
  149. define('debug', default=False, help='run in debug mode', type=bool)
  150. tornado.options.parse_command_line()
  151. if options.debug:
  152. logger.setLevel(logging.DEBUG)
  153. ssl_options = {}
  154. if os.path.isfile(options.ssl_key) and os.path.isfile(options.ssl_cert):
  155. ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
  156. init_params = dict(listen_port=options.port, logger=logger, ssl_options=ssl_options)
  157. _upload_path = r'upload/?'
  158. application = tornado.web.Application([
  159. (r'/api/%s' % _upload_path, UploadHandler, init_params),
  160. (r'/api/v%s/%s' % (API_VERSION, _upload_path), UploadHandler, init_params),
  161. (r'/?(.*)', TemplateHandler, init_params),
  162. ],
  163. static_path=os.path.join(os.path.dirname(__file__), 'dist/static'),
  164. template_path=os.path.join(os.path.dirname(__file__), 'dist/'),
  165. debug=options.debug)
  166. http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None)
  167. logger.info('Start serving on %s://%s:%d', 'https' if ssl_options else 'http',
  168. options.address if options.address else '127.0.0.1',
  169. options.port)
  170. http_server.listen(options.port, options.address)
  171. try:
  172. IOLoop.instance().start()
  173. except (KeyboardInterrupt, SystemExit):
  174. pass
  175. if __name__ == '__main__':
  176. serve()