├── Readme.md ├── droopy ├── img ├── droopy-in-browser-thumb.png └── droopy-in-terminal-thumb.png └── man └── droopy.1 /Readme.md: -------------------------------------------------------------------------------- 1 | # Droopy 2 | ### Easy File Sharing 3 | Copyright 2008-2013 (c) Pierre Duquesne 4 | Licensed under the New BSD License. 5 | Originally shared at [Pierre's Blog, stackp.online.fr](http://stackp.online.fr/droopy). 6 | 7 | ### About 8 | Droopy is a standalone, minimal file-sharing server written in pure Python and 9 | designed for simplicity. Originally written to facilitate easy sharing on an 10 | ad-hoc basis, it has become central to systems such as [Piratebox](http://www.piratebox.cc/) 11 | which extend the concept to offline, social filesharing. 12 | 13 | ![Droopy in the Browser, with a title image](img/droopy-in-browser-thumb.png) 14 | 15 | ### Usage 16 | Note: [A tutorial on how to set up Droopy on Windows](http://www.techkings.org/general-pc-chat/34104-droopy-tutorial.html) 17 | was very kindly written by Ronan. The rest of this section focuses on Linux and MacOSX. 18 | 19 | Droopy is a command-line program. I’ll suppose you’ve downloaded and saved the file in `~/bin/`. 20 | Go to the directory where you want the uploaded files to be stored, for example: 21 | 22 | mkdir ~/uploads 23 | cd ~/uploads 24 | 25 | Then, run droopy. You can give a message and a picture to display: 26 | 27 | python3 ~/bin/droopy -m "Hi, it's me Bob. You can send me a file." -p ~/avatar.png 28 | 29 | ![Droopy at the terminal](img/droopy-in-terminal-thumb.png) 30 | 31 | And it’s up and running on port 8000 of you computer. Check it out at `http://localhost:8000`, 32 | and give your computer’s address to your friends. 33 | 34 | Droopy supports a number of other options; try `python3 droopy --help` for insight. 35 | On Linux, droopy can be run directly without specifically calling `python3`, so 36 | you can put `droopy` into a folder that's in your system PATH variable, such as 37 | `/usr/bin/` and call it directly: `droopy --help`. 38 | 39 | ### Feedback and contribution 40 | I’d love to hear about your experience using droopy. 41 | If you have ideas to improve it, please let me know. 42 | Pierre – [stackp@online.fr](mailto:stackp@online.fr). 43 | -------------------------------------------------------------------------------- /droopy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Droopy (http://stackp.online.fr/droopy) 5 | Copyright 2008-2013 (c) Pierre Duquesne 6 | Licensed under the New BSD License. 7 | 8 | Changelog 9 | 20151025 * Global variables removed 10 | * Code refactoring and re-layout 11 | * Python 2 and 3 compatibility 12 | * Efficiency and Security improvements 13 | * Added --config-file option. 14 | * Retains backwards compatibility. 15 | 20131121 * Update HTML/CSS for mobile devices 16 | * Add HTTPS support 17 | * Add HTTP basic authentication 18 | * Add option to change uploaded file permissions 19 | * Add support for HTML5 multiple file upload 20 | 20120108 * Taiwanese translation by Li-cheng Hsu. 21 | 20110928 * Correctly save message with --save-config. Fix by Sven Radde. 22 | 20110708 * Polish translation by Jacek Politowski. 23 | 20110625 * Fix bug regarding filesystem name encoding. 24 | * Save the --dl option when --save-config is passed. 25 | 20110501 * Add the --dl option to let clients download files. 26 | * CSS speech bubble. 27 | 20101130 * CSS and HTML update. Switch to the new BSD License. 28 | 20100523 * Simplified Chinese translation by Ye Wei. 29 | 20100521 * Hungarian translation by Csaba Szigetvári. 30 | * Russian translation by muromec. 31 | * Use %APPDATA% Windows environment variable -- fix by Maik. 32 | 20091229 * Brazilian Portuguese translation by 33 | Carlos Eduardo Moreira dos Santos and Toony Poony. 34 | * IE layout fix by Carlos Eduardo Moreira dos Santos. 35 | * Galician translation by Miguel Anxo Bouzada. 36 | 20090721 * Indonesian translation by Kemas. 37 | 20090205 * Japanese translation by Satoru Matsumoto. 38 | * Slovak translation by CyberBoBaK. 39 | 20090203 * Norwegian translation by Preben Olav Pedersen. 40 | 20090202 * Korean translation by xissy. 41 | * Fix for unicode filenames by xissy. 42 | * Relies on 127.0.0.1 instead of "localhost" hostname. 43 | 20090129 * Serbian translation by kotnik. 44 | 20090125 * Danish translation by jan. 45 | 20081210 * Greek translation by n2j3. 46 | 20081128 * Slovene translation by david. 47 | * Romanian translation by Licaon. 48 | 20081022 * Swedish translation by David Eurenius. 49 | 20081001 * Droopy gets pretty (css and html rework). 50 | * Finnish translation by ipppe. 51 | 20080926 * Configuration saving and loading. 52 | 20080906 * Extract the file base name (some browsers send the full path). 53 | 20080905 * File is uploaded directly into the specified directory. 54 | 20080904 * Arabic translation by Djalel Chefrour. 55 | * Italian translation by fabius and d1s4st3r. 56 | * Dutch translation by Tonio Voerman. 57 | * Portuguese translation by Pedro Palma. 58 | * Turkish translation by Heartsmagic. 59 | 20080727 * Spanish translation by Federico Kereki. 60 | 20080624 * Option -d or --directory to specify the upload directory. 61 | 20080622 * File numbering to avoid overwriting. 62 | 20080620 * Czech translation by Jiří. 63 | * German translation by Michael. 64 | 20080408 * First release. 65 | """ 66 | from __future__ import print_function 67 | import sys 68 | if sys.version_info >= (3, 0): 69 | from http import server as httpserver 70 | import socketserver 71 | from urllib import parse as urllibparse 72 | unicode = str 73 | else: 74 | import BaseHTTPServer as httpserver 75 | import SocketServer as socketserver 76 | import urllib as urllibparse 77 | 78 | import cgi 79 | import os 80 | import posixpath 81 | import os.path 82 | import ntpath 83 | import argparse 84 | import mimetypes 85 | import shutil 86 | import tempfile 87 | import socket 88 | import base64 89 | import functools 90 | 91 | 92 | def _decode_str_if_py2(inputstr, encoding='utf-8'): 93 | "Will return decoded with given encoding *if* input is a string and it's Py2." 94 | if sys.version_info < (3,) and isinstance(inputstr, str): 95 | return inputstr.decode(encoding) 96 | else: 97 | return inputstr 98 | 99 | def _encode_str_if_py2(inputstr, encoding='utf-8'): 100 | "Will return encoded with given encoding *if* input is a string and it's Py2" 101 | if sys.version_info < (3,) and isinstance(inputstr, str): 102 | return inputstr.encode(encoding) 103 | else: 104 | return inputstr 105 | 106 | def fullpath(path): 107 | "Shortcut for os.path abspath(expanduser())" 108 | return os.path.abspath(os.path.expanduser(path)) 109 | 110 | def basename(path): 111 | "Extract the file base name (some browsers send the full file path)." 112 | for mod in posixpath, os.path, ntpath: 113 | path = mod.basename(path) 114 | return path 115 | 116 | def check_auth(method): 117 | "Wraps methods on the request handler to require simple auth checks." 118 | def decorated(self, *pargs): 119 | "Reject if auth fails." 120 | if self.auth: 121 | # TODO: Between minor versions this handles str/bytes differently 122 | received = self.get_case_insensitive_header('Authorization', None) 123 | expected = 'Basic ' + base64.b64encode(self.auth) 124 | # TODO: Timing attack? 125 | if received != expected: 126 | self.send_response(401) 127 | self.send_header('WWW-Authenticate', 'Basic realm=\"Droopy\"') 128 | self.send_header('Content-type', 'text/html') 129 | self.end_headers() 130 | else: 131 | method(self, *pargs) 132 | else: 133 | method(self, *pargs) 134 | functools.update_wrapper(decorated, method) 135 | return decorated 136 | 137 | 138 | class Abort(Exception): 139 | "Used by handle to rethrow exceptions in ThreadedHTTPServer." 140 | 141 | 142 | class DroopyFieldStorage(cgi.FieldStorage): 143 | """ 144 | The file is created in the destination directory and its name is 145 | stored in the tmpfilename attribute. 146 | 147 | Adds a keyword-argument "directory", which is where files are to be 148 | stored. Because of CGI magic this might not be thread-safe. 149 | """ 150 | 151 | TMPPREFIX = 'tmpdroopy' 152 | 153 | # Would love to do a **kwargs job here but cgi has some recursive 154 | # magic that passes all possible arguments positionally.. 155 | def __init__(self, fp=None, headers=None, outerboundary=b'', 156 | environ=os.environ, keep_blank_values=0, strict_parsing=0, 157 | limit=None, encoding='utf-8', errors='replace', 158 | directory='.'): 159 | """ 160 | Adds 'directory' argument to FieldStorage.__init__. 161 | Retains compatibility with FieldStorage.__init__ (which involves magic) 162 | """ 163 | self.directory = directory 164 | # Not only is cgi.FieldStorage full of magic, it's DIFFERENT 165 | # magic in Py2/Py3. Here's a case of the core library making 166 | # life difficult, in a class that's *supposed to be subclassed*! 167 | if sys.version_info > (3,): 168 | cgi.FieldStorage.__init__(self, fp, headers, outerboundary, 169 | environ, keep_blank_values, 170 | strict_parsing, limit, encoding, errors) 171 | else: 172 | cgi.FieldStorage.__init__(self, fp, headers, outerboundary, 173 | environ, keep_blank_values, 174 | strict_parsing) 175 | 176 | # Binary is passed in Py2 but not Py3. 177 | def make_file(self, binary=None): 178 | "Overrides builtin method to store tempfile in the set directory." 179 | fd, name = tempfile.mkstemp(dir=self.directory, prefix=self.TMPPREFIX) 180 | # Pylint doesn't like these if they're not declared in __init__ first, 181 | # but setting tmpfile there leads to odd errors where it's never re-set 182 | # to a file descriptor. 183 | self.tmpfile = os.fdopen(fd, 'w+b') 184 | self.tmpfilename = name 185 | return self.tmpfile 186 | 187 | 188 | class HTTPUploadHandler(httpserver.BaseHTTPRequestHandler): 189 | "The guts of Droopy-a custom handler that accepts files & serves templates" 190 | 191 | @property 192 | def templates(self): 193 | "Ensure provided." 194 | raise NotImplementedError("Must set class with a templates dict!") 195 | 196 | @property 197 | def localisations(self): 198 | "Ensure provided." 199 | raise NotImplementedError("Must set class with a localisations dict!") 200 | 201 | @property 202 | def directory(self): 203 | "Ensure provided." 204 | raise NotImplementedError("Must provide directory to host.") 205 | 206 | message = '' 207 | picture = '' 208 | publish_files = False 209 | file_mode = None 210 | protocol_version = 'HTTP/1.0' 211 | form_field = 'upfile' 212 | auth = '' 213 | certfile = None 214 | divpicture = '
' 215 | 216 | def get_case_insensitive_header(self, hdrname, default): 217 | "Python 2 and 3 differ in header capitalisation!" 218 | lc_hdrname = hdrname.lower() 219 | lc_headers = dict((h.lower(), h) for h in self.headers.keys()) 220 | if lc_hdrname in lc_headers: 221 | return self.headers[lc_headers[lc_hdrname]] 222 | else: 223 | return default 224 | 225 | @staticmethod 226 | def prefcode_tuple(prefcode): 227 | "Parse language preferences into (preference, language) tuples." 228 | prefbits = prefcode.split(";q=") 229 | if len(prefbits) == 1: 230 | return (1, prefbits[0]) 231 | else: 232 | return (float(prefbits[1]), prefbits[0]) 233 | 234 | def parse_accepted_languages(self): 235 | "Parse accept-language header" 236 | lhdr = self.get_case_insensitive_header('accept-language', default='') 237 | if lhdr: 238 | accepted = [self.prefcode_tuple(lang) for lang in lhdr.split(',')] 239 | accepted.sort() 240 | accepted.reverse() 241 | return [x[1] for x in accepted] 242 | else: 243 | return [] 244 | 245 | def choose_language(self): 246 | "Choose localisation based on accept-language header (default 'en')" 247 | accepted = self.parse_accepted_languages() 248 | # -- Choose the appropriate translation dictionary (default is english) 249 | lang = "en" 250 | for alang in accepted: 251 | if alang in self.localisations: 252 | lang = alang 253 | break 254 | return self.localisations[lang] 255 | 256 | def html(self, page): 257 | """ 258 | page can be "main", "success", or "error" 259 | returns an html page (in the appropriate language) as a string 260 | """ 261 | dico = {} 262 | dico.update(self.choose_language()) 263 | # -- Set message and picture 264 | if self.message: 265 | dico['message'] = '
{0}
'.format(self.message) 266 | else: 267 | dico["message"] = '' 268 | # The default appears to be missing/broken, so needs a bit of love anyway. 269 | if self.picture: 270 | dico["divpicture"] = self.divpicture 271 | else: 272 | dico["divpicture"] = '' 273 | # -- Possibly provide download links 274 | # TODO: Sanity-check for injections 275 | links = '' 276 | if self.publish_files: 277 | for name in self.published_files(): 278 | encoded_name = urllibparse.quote(_encode_str_if_py2(name)) 279 | links += '{1}'.format(encoded_name, name) 280 | links = '
' + links + '
' 281 | dico["files"] = links 282 | # -- Add a link to discover the url 283 | if self.client_address[0] == "127.0.0.1": 284 | dico["port"] = self.server.server_port 285 | dico["ssl"] = int(self.certfile is not None) 286 | dico["linkurl"] = self.templates['linkurl'] % dico 287 | else: 288 | dico["linkurl"] = '' 289 | return self.templates[page] % dico 290 | 291 | @check_auth 292 | def do_GET(self): 293 | "Standard method to override in this Server object." 294 | name = self.path.lstrip('/') 295 | name = urllibparse.unquote(name) 296 | name = _decode_str_if_py2(name, 'utf-8') 297 | 298 | # TODO: Refactor special-method handling to make more modular? 299 | # Include ability to self-define "special method" prefix path? 300 | if self.picture != None and self.path == '/__droopy/picture': 301 | # send the picture 302 | self.send_file(self.picture) 303 | # TODO Verify that this is path-injection proof 304 | elif name in self.published_files(): 305 | localpath = os.path.join(self.directory, name) 306 | self.send_file(localpath) 307 | else: 308 | self.send_html(self.html("main")) 309 | 310 | @check_auth 311 | def do_POST(self): 312 | "Standard method to override in this Server object." 313 | try: 314 | self.log_message("Started file transfer") 315 | # -- Save file (numbered to avoid overwriting, ex: foo-3.png) 316 | form = DroopyFieldStorage(fp=self.rfile, 317 | directory=self.directory, 318 | headers=self.headers, 319 | environ={'REQUEST_METHOD': self.command}) 320 | file_items = form[self.form_field] 321 | #-- Handle multiple file upload 322 | if not isinstance(file_items, list): 323 | file_items = [file_items] 324 | for item in file_items: 325 | filename = _decode_str_if_py2(basename(item.filename), "utf-8") 326 | if filename == "": 327 | continue 328 | localpath = _encode_str_if_py2(os.path.join(self.directory, filename), "utf-8") 329 | root, ext = os.path.splitext(localpath) 330 | i = 1 331 | # TODO: race condition... 332 | while os.path.exists(localpath): 333 | localpath = "%s-%d%s" % (root, i, ext) 334 | i = i + 1 335 | if hasattr(item, 'tmpfile'): 336 | # DroopyFieldStorage.make_file() has been called 337 | item.tmpfile.close() 338 | shutil.move(item.tmpfilename, localpath) 339 | else: 340 | # no temporary file, self.file is a StringIO() 341 | # see cgi.FieldStorage.read_lines() 342 | with open(localpath, "wb") as fout: 343 | shutil.copyfileobj(item.file, fout) 344 | if self.file_mode is not None: 345 | os.chmod(localpath, self.file_mode) 346 | self.log_message("Received: %s", os.path.basename(localpath)) 347 | 348 | # -- Reply 349 | if self.publish_files: 350 | # The file list gives a feedback for the upload success 351 | self.send_resp_headers(301, {'Location': '/'}, end=True) 352 | else: 353 | self.send_html(self.html("success")) 354 | 355 | except Exception as e: 356 | self.log_message(repr(e)) 357 | self.send_html(self.html("error")) 358 | # raise e # Dev only 359 | 360 | def send_resp_headers(self, response_code, headers_dict, end=False): 361 | "Just a shortcut for a common operation." 362 | self.send_response(response_code) 363 | for k, v in headers_dict.items(): 364 | self.send_header(k, v) 365 | if end: 366 | self.end_headers() 367 | 368 | def send_html(self, htmlstr): 369 | "Simply returns htmlstr with the appropriate content-type/status." 370 | self.send_resp_headers(200, {'Content-type': 'text/html; charset=utf-8'}, end=True) 371 | self.wfile.write(htmlstr.encode("utf-8")) 372 | 373 | def send_file(self, localpath): 374 | "Does what it says on the tin! Includes correct content-type/length." 375 | with open(localpath, 'rb') as f: 376 | self.send_resp_headers(200, 377 | {'Content-length': os.fstat(f.fileno())[6], 378 | 'Content-type': mimetypes.guess_type(localpath)[0]}, 379 | end=True) 380 | shutil.copyfileobj(f, self.wfile) 381 | 382 | def published_files(self): 383 | "Returns the list of files that should appear as download links." 384 | names = [] 385 | # In py2, listdir() returns strings when the directory is a string. 386 | for name in os.listdir(unicode(self.directory)): 387 | if name.startswith(DroopyFieldStorage.TMPPREFIX): 388 | continue 389 | npath = os.path.join(self.directory, name) 390 | if os.path.isfile(npath): 391 | names.append(name) 392 | names.sort(key=lambda s: s.lower()) 393 | return names 394 | 395 | def handle(self): 396 | "Lets parent object handle, but redirects socket exceptions as 'Abort's." 397 | try: 398 | httpserver.BaseHTTPRequestHandler.handle(self) 399 | except socket.error as e: 400 | self.log_message(str(e)) 401 | raise Abort(str(e)) 402 | 403 | 404 | class ThreadedHTTPServer(socketserver.ThreadingMixIn, 405 | httpserver.HTTPServer): 406 | "Allows propagation of socket.error in HTTPUploadHandler.handle" 407 | def handle_error(self, request, client_address): 408 | "Override socketserver.handle_error" 409 | exctype = sys.exc_info()[0] 410 | if not exctype is Abort: 411 | httpserver.HTTPServer.handle_error(self, request, client_address) 412 | 413 | 414 | def run(hostname='', 415 | port=80, 416 | templates=None, 417 | localisations=None, 418 | directory='.', 419 | timeout=3*60, 420 | picture=None, 421 | message='', 422 | file_mode=None, 423 | publish_files=False, 424 | auth='', 425 | certfile=None, 426 | permitted_ciphers=( 427 | 'ECDH+AESGCM:ECDH+AES256:ECDH+AES128:ECDH+3DES' 428 | ':RSA+AESGCM:RSA+AES:RSA+3DES' 429 | ':!aNULL:!MD5:!DSS')): 430 | """ 431 | certfile should be the path of a PEM TLS certificate. 432 | 433 | permitted_ciphers, if a TLS cert is provided, is an OpenSSL cipher string. 434 | The default here is taken from: 435 | https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ 436 | ..with DH-only ciphers removed because of precomputation hazard. 437 | """ 438 | if templates is None or localisations is None: 439 | raise ValueError("Must provide templates *and* localisations.") 440 | socket.setdefaulttimeout(timeout) 441 | HTTPUploadHandler.templates = templates 442 | HTTPUploadHandler.directory = directory 443 | HTTPUploadHandler.localisations = localisations 444 | HTTPUploadHandler.certfile = certfile 445 | HTTPUploadHandler.publish_files = publish_files 446 | HTTPUploadHandler.picture = picture 447 | HTTPUploadHandler.message = message 448 | HTTPUploadHandler.file_mode = file_mode 449 | HTTPUploadHandler.auth = auth 450 | httpd = ThreadedHTTPServer((hostname, port), HTTPUploadHandler) 451 | # TODO: Specify TLS1.2 only? 452 | if certfile: 453 | try: 454 | import ssl 455 | except: 456 | print("Error: Could not import module 'ssl', exiting.") 457 | sys.exit(2) 458 | httpd.socket = ssl.wrap_socket( 459 | httpd.socket, 460 | certfile=certfile, 461 | ciphers=permitted_ciphers, 462 | server_side=True) 463 | httpd.serve_forever() 464 | 465 | # -- Dato 466 | 467 | # -- HTML templates 468 | 469 | style = ''' 470 | 472 | ''' 508 | 509 | userinfo = ''' 510 |
511 | %(message)s 512 | %(divpicture)s 513 |
514 | ''' 515 | 516 | maintmpl = ''' 517 | 518 | 519 | 520 | %(maintitle)s 521 | ''' + style + ''' 522 | 543 | 544 | %(linkurl)s 545 |
546 |
547 |
548 |
549 | 550 | 551 |
552 |
553 |
554 | 555 | 556 | 559 |
557 | 558 |
560 |
%(sending)s
561 |
562 |
563 | ''' + userinfo + ''' 564 | %(files)s 565 |
566 | 567 | 568 | ''' 569 | 570 | successtmpl = ''' 571 | 572 | 573 | %(successtitle)s 574 | ''' + style + ''' 575 | 576 | 577 |
578 |
579 |
580 | %(received)s 581 | %(another)s 582 |
583 |
584 | ''' + userinfo + ''' 585 |
586 | 587 | 588 | ''' 589 | 590 | errortmpl = ''' 591 | 592 | 593 | %(errortitle)s 594 | ''' + style + ''' 595 | 596 | 597 |
598 |
599 |
600 | %(problem)s 601 | %(retry)s 602 |
603 |
604 | ''' + userinfo + ''' 605 |
606 | 607 | 608 | ''' 609 | 610 | linkurltmpl = '''''' 613 | 614 | 615 | default_templates = { 616 | "main": maintmpl, 617 | "success": successtmpl, 618 | "error": errortmpl, 619 | "linkurl": linkurltmpl} 620 | 621 | # -- Translations 622 | default_localisations = { 623 | 'ar' : { 624 | "maintitle": u"إرسال ملف", 625 | "submit": u"إرسال", 626 | "sending": u"الملف قيد الإرسال", 627 | "successtitle": u"تم استقبال الملف", 628 | "received": u"تم استقبال الملف !", 629 | "another": u"إرسال ملف آخر", 630 | "errortitle": u"مشكلة", 631 | "problem": u"حدثت مشكلة !", 632 | "retry": u"إعادة المحاولة", 633 | "discover": u"اكتشاف عنوان هذه الصفحة"}, 634 | 'cs' : { 635 | "maintitle": u"Poslat soubor", 636 | "submit": u"Poslat", 637 | "sending": u"Posílám", 638 | "successtitle": u"Soubor doručen", 639 | "received": u"Soubor doručen !", 640 | "another": u"Poslat další soubor", 641 | "errortitle": u"Chyba", 642 | "problem": u"Stala se chyba !", 643 | "retry": u"Zkusit znova.", 644 | "discover": u"Zjistit adresu stránky"}, 645 | 'da' : { 646 | "maintitle": u"Send en fil", 647 | "submit": u"Send", 648 | "sending": u"Sender", 649 | "successtitle": u"Fil modtaget", 650 | "received": u"Fil modtaget!", 651 | "another": u"Send en fil til.", 652 | "errortitle": u"Problem", 653 | "problem": u"Det er opstået en fejl!", 654 | "retry": u"Forsøg igen.", 655 | "discover": u"Find adressen til denne side"}, 656 | 'de' : { 657 | "maintitle": "Datei senden", 658 | "submit": "Senden", 659 | "sending": "Sendet", 660 | "successtitle": "Datei empfangen", 661 | "received": "Datei empfangen!", 662 | "another": "Weitere Datei senden", 663 | "errortitle": "Fehler", 664 | "problem": "Ein Fehler ist aufgetreten!", 665 | "retry": "Wiederholen", 666 | "discover": "Internet-Adresse dieser Seite feststellen"}, 667 | 'el' : { 668 | "maintitle": u"Στείλε ένα αρχείο", 669 | "submit": u"Αποστολή", 670 | "sending": u"Αποστέλλεται...", 671 | "successtitle": u"Επιτυχής λήψη αρχείου ", 672 | "received": u"Λήψη αρχείου ολοκληρώθηκε", 673 | "another": u"Στείλε άλλο ένα αρχείο", 674 | "errortitle": u"Σφάλμα", 675 | "problem": u"Παρουσιάστηκε σφάλμα", 676 | "retry": u"Επανάληψη", 677 | "discover": u"Βρες την διεύθυνση της σελίδας"}, 678 | 'en' : { 679 | "maintitle": "Send a file", 680 | "submit": "Send", 681 | "sending": "Sending", 682 | "successtitle": "File received", 683 | "received": "File received!", 684 | "another": "Send another file.", 685 | "errortitle": "Problem", 686 | "problem": "There has been a problem!", 687 | "retry": "Retry.", 688 | "discover": "Discover the address of this page"}, 689 | 'es' : { 690 | "maintitle": u"Enviar un archivo", 691 | "submit": u"Enviar", 692 | "sending": u"Enviando", 693 | "successtitle": u"Archivo recibido", 694 | "received": u"¡Archivo recibido!", 695 | "another": u"Enviar otro archivo.", 696 | "errortitle": u"Error", 697 | "problem": u"¡Hubo un problema!", 698 | "retry": u"Reintentar", 699 | "discover": u"Descubrir la dirección de esta página"}, 700 | 'fi' : { 701 | "maintitle": u"Lähetä tiedosto", 702 | "submit": u"Lähetä", 703 | "sending": u"Lähettää", 704 | "successtitle": u"Tiedosto vastaanotettu", 705 | "received": u"Tiedosto vastaanotettu!", 706 | "another": u"Lähetä toinen tiedosto.", 707 | "errortitle": u"Virhe", 708 | "problem": u"Virhe lahetettäessä tiedostoa!", 709 | "retry": u"Uudelleen.", 710 | "discover": u"Näytä tämän sivun osoite"}, 711 | 'fr' : { 712 | "maintitle": u"Envoyer un fichier", 713 | "submit": u"Envoyer", 714 | "sending": u"Envoi en cours", 715 | "successtitle": u"Fichier reçu", 716 | "received": u"Fichier reçu !", 717 | "another": u"Envoyer un autre fichier.", 718 | "errortitle": u"Problème", 719 | "problem": u"Il y a eu un problème !", 720 | "retry": u"Réessayer.", 721 | "discover": u"Découvrir l'adresse de cette page"}, 722 | 'gl' : { 723 | "maintitle": u"Enviar un ficheiro", 724 | "submit": u"Enviar", 725 | "sending": u"Enviando", 726 | "successtitle": u"Ficheiro recibido", 727 | "received": u"Ficheiro recibido!", 728 | "another": u"Enviar outro ficheiro.", 729 | "errortitle": u"Erro", 730 | "problem": u"Xurdíu un problema!", 731 | "retry": u"Reintentar", 732 | "discover": u"Descubrir o enderezo desta páxina"}, 733 | 'hu' : { 734 | "maintitle": u"Állomány küldése", 735 | "submit": u"Küldés", 736 | "sending": u"Küldés folyamatban", 737 | "successtitle": u"Az állomány beérkezett", 738 | "received": u"Az állomány beérkezett!", 739 | "another": u"További állományok küldése", 740 | "errortitle": u"Hiba", 741 | "problem": u"Egy hiba lépett fel!", 742 | "retry": u"Megismételni", 743 | "discover": u"Az oldal Internet-címének megállapítása"}, 744 | 'id' : { 745 | "maintitle": "Kirim sebuah berkas", 746 | "submit": "Kirim", 747 | "sending": "Mengirim", 748 | "successtitle": "Berkas diterima", 749 | "received": "Berkas diterima!", 750 | "another": "Kirim berkas yang lain.", 751 | "errortitle": "Permasalahan", 752 | "problem": "Telah ditemukan sebuah kesalahan!", 753 | "retry": "Coba kembali.", 754 | "discover": "Kenali alamat IP dari halaman ini"}, 755 | 'it' : { 756 | "maintitle": u"Invia un file", 757 | "submit": u"Invia", 758 | "sending": u"Invio in corso", 759 | "successtitle": u"File ricevuto", 760 | "received": u"File ricevuto!", 761 | "another": u"Invia un altro file.", 762 | "errortitle": u"Errore", 763 | "problem": u"Si è verificato un errore!", 764 | "retry": u"Riprova.", 765 | "discover": u"Scopri l’indirizzo di questa pagina"}, 766 | 'ja' : { 767 | "maintitle": u"ファイル送信", 768 | "submit": u"送信", 769 | "sending": u"送信中", 770 | "successtitle": u"受信完了", 771 | "received": u"ファイルを受信しました!", 772 | "another": u"他のファイルを送信する", 773 | "errortitle": u"問題発生", 774 | "problem": u"問題が発生しました!", 775 | "retry": u"リトライ", 776 | "discover": u"このページのアドレスを確認する"}, 777 | 'ko' : { 778 | "maintitle": u"파일 보내기", 779 | "submit": u"보내기", 780 | "sending": u"보내는 중", 781 | "successtitle": u"파일이 받아졌습니다", 782 | "received": u"파일이 받아졌습니다!", 783 | "another": u"다른 파일 보내기", 784 | "errortitle": u"문제가 발생했습니다", 785 | "problem": u"문제가 발생했습니다!", 786 | "retry": u"다시 시도", 787 | "discover": u"이 페이지 주소 알아보기"}, 788 | 'nl' : { 789 | "maintitle": "Verstuur een bestand", 790 | "submit": "Verstuur", 791 | "sending": "Bezig met versturen", 792 | "successtitle": "Bestand ontvangen", 793 | "received": "Bestand ontvangen!", 794 | "another": "Verstuur nog een bestand.", 795 | "errortitle": "Fout", 796 | "problem": "Er is een fout opgetreden!", 797 | "retry": "Nog eens.", 798 | "discover": "Vind het adres van deze pagina"}, 799 | 'no' : { 800 | "maintitle": u"Send en fil", 801 | "submit": u"Send", 802 | "sending": u"Sender", 803 | "successtitle": u"Fil mottatt", 804 | "received": u"Fil mottatt !", 805 | "another": u"Send en ny fil.", 806 | "errortitle": u"Feil", 807 | "problem": u"Det har skjedd en feil !", 808 | "retry": u"Send på nytt.", 809 | "discover": u"Finn addressen til denne siden"}, 810 | 'pl' : { 811 | "maintitle": u"Wyślij plik", 812 | "submit": u"Wyślij", 813 | "sending": u"Wysyłanie", 814 | "successtitle": u"Plik wysłany", 815 | "received": u"Plik wysłany!", 816 | "another": u"Wyślij kolejny plik.", 817 | "errortitle": u"Problem", 818 | "problem": u"Wystąpił błąd!", 819 | "retry": u"Spróbuj ponownie.", 820 | "discover": u"Znajdź adres tej strony"}, 821 | 'pt' : { 822 | "maintitle": u"Enviar um ficheiro", 823 | "submit": u"Enviar", 824 | "sending": u"A enviar", 825 | "successtitle": u"Ficheiro recebido", 826 | "received": u"Ficheiro recebido !", 827 | "another": u"Enviar outro ficheiro.", 828 | "errortitle": u"Erro", 829 | "problem": u"Ocorreu um erro !", 830 | "retry": u"Tentar novamente.", 831 | "discover": u"Descobrir o endereço desta página"}, 832 | 'pt-br' : { 833 | "maintitle": u"Enviar um arquivo", 834 | "submit": u"Enviar", 835 | "sending": u"Enviando", 836 | "successtitle": u"Arquivo recebido", 837 | "received": u"Arquivo recebido!", 838 | "another": u"Enviar outro arquivo.", 839 | "errortitle": u"Erro", 840 | "problem": u"Ocorreu um erro!", 841 | "retry": u"Tentar novamente.", 842 | "discover": u"Descobrir o endereço desta página"}, 843 | 'ro' : { 844 | "maintitle": u"Trimite un fişier", 845 | "submit": u"Trimite", 846 | "sending": u"Se trimite", 847 | "successtitle": u"Fişier recepţionat", 848 | "received": u"Fişier recepţionat !", 849 | "another": u"Trimite un alt fişier.", 850 | "errortitle": u"Problemă", 851 | "problem": u"A intervenit o problemă !", 852 | "retry": u"Reîncearcă.", 853 | "discover": u"Descoperă adresa acestei pagini"}, 854 | 'ru' : { 855 | "maintitle": u"Отправить файл", 856 | "submit": u"Отправить", 857 | "sending": u"Отправляю", 858 | "successtitle": u"Файл получен", 859 | "received": u"Файл получен !", 860 | "another": u"Отправить другой файл.", 861 | "errortitle": u"Ошибка", 862 | "problem": u"Произошла ошибка !", 863 | "retry": u"Повторить.", 864 | "discover": u"Посмотреть адрес этой страницы"}, 865 | 'sk' : { 866 | "maintitle": u"Pošli súbor", 867 | "submit": u"Pošli", 868 | "sending": u"Posielam", 869 | "successtitle": u"Súbor prijatý", 870 | "received": u"Súbor prijatý !", 871 | "another": u"Poslať ďalší súbor.", 872 | "errortitle": u"Chyba", 873 | "problem": u"Vyskytla sa chyba!", 874 | "retry": u"Skúsiť znova.", 875 | "discover": u"Zisti adresu tejto stránky"}, 876 | 'sl' : { 877 | "maintitle": u"Pošlji datoteko", 878 | "submit": u"Pošlji", 879 | "sending": u"Pošiljam", 880 | "successtitle": u"Datoteka prejeta", 881 | "received": u"Datoteka prejeta !", 882 | "another": u"Pošlji novo datoteko.", 883 | "errortitle": u"Napaka", 884 | "problem": u"Prišlo je do napake !", 885 | "retry": u"Poizkusi ponovno.", 886 | "discover": u"Poišči naslov na tej strani"}, 887 | 'sr' : { 888 | "maintitle": u"Pošalji fajl", 889 | "submit": u"Pošalji", 890 | "sending": u"Šaljem", 891 | "successtitle": u"Fajl primljen", 892 | "received": u"Fajl primljen !", 893 | "another": u"Pošalji još jedan fajl.", 894 | "errortitle": u"Problem", 895 | "problem": u"Desio se problem !", 896 | "retry": u"Pokušaj ponovo.", 897 | "discover": u"Otkrij adresu ove stranice"}, 898 | 'sv' : { 899 | "maintitle": u"Skicka en fil", 900 | "submit": u"Skicka", 901 | "sending": u"Skickar...", 902 | "successtitle": u"Fil mottagen", 903 | "received": u"Fil mottagen !", 904 | "another": u"Skicka en fil till.", 905 | "errortitle": u"Fel", 906 | "problem": u"Det har uppstått ett fel !", 907 | "retry": u"Försök igen.", 908 | "discover": u"Ta reda på adressen till denna sida"}, 909 | 'tr' : { 910 | "maintitle": u"Dosya gönder", 911 | "submit": u"Gönder", 912 | "sending": u"Gönderiliyor...", 913 | "successtitle": u"Gönderildi", 914 | "received": u"Gönderildi", 915 | "another": u"Başka bir dosya gönder.", 916 | "errortitle": u"Problem.", 917 | "problem": u"Bir problem oldu !", 918 | "retry": u"Yeniden dene.", 919 | "discover": u"Bu sayfanın adresini bul"}, 920 | 'zh-cn' : { 921 | "maintitle": u"发送文件", 922 | "submit": u"发送", 923 | "sending": u"发送中", 924 | "successtitle": u"文件已收到", 925 | "received": u"文件已收到!", 926 | "another": u"发送另一个文件。", 927 | "errortitle": u"问题", 928 | "problem": u"出现问题!", 929 | "retry": u"重试。", 930 | "discover": u"查看本页面的地址"}, 931 | 'zh-tw' : { 932 | "maintitle": u"上傳檔案", 933 | "submit": u"上傳", 934 | "sending": u"傳送中...", 935 | "successtitle": u"已收到檔案", 936 | "received": u"已收到檔案!", 937 | "another": u"上傳另一個檔案。", 938 | "errortitle": u"錯誤", 939 | "problem": u"出現錯誤!", 940 | "retry": u"重試。", 941 | "discover": u"查閱本網頁的網址"} 942 | } # Ends default_localisations dictionary. 943 | 944 | 945 | # -- Options 946 | 947 | def default_configfile(): 948 | "Returns appropriate absolute path to configfile, per-platform." 949 | appname = 'droopy' 950 | if os.name == 'posix': 951 | filename = os.path.join(os.environ['HOME'], "." + appname) 952 | elif os.name == 'mac': 953 | filename = os.path.join(os.environ['HOME'], 'Library', 'Application Support', appname) 954 | elif os.name == 'nt': 955 | filename = os.path.join(os.environ['APPDATA'], appname) 956 | else: 957 | # Exaggerated shrug 958 | filename = './' + appname 959 | return filename 960 | 961 | 962 | def save_options(cfg): 963 | "Dumps sys.argv with one argument per line." 964 | with open(cfg, "w") as O: 965 | ignorenext = False 966 | for opt in sys.argv[1:]: 967 | if ignorenext: 968 | ignorenext = False 969 | continue 970 | if opt.startswith("-"): 971 | if opt.strip() in ("--save-config", "--delete-config"): 972 | continue 973 | if opt.strip() == '--config-file': 974 | ignorenext = True 975 | continue 976 | O.write("\n") 977 | else: 978 | O.write(" ") 979 | O.write(opt) 980 | 981 | 982 | def load_options(cfg_loc): 983 | """ 984 | Attempts to open location, piece lines back together into a terminal-style 985 | invocation, and pass to parse_args. 986 | """ 987 | try: 988 | with open(cfg_loc) as f: 989 | cmd = [] 990 | for line in f: 991 | line = line.strip() 992 | if not line: 993 | continue 994 | if line.startswith("-"): 995 | if " " in line: 996 | opt, rest = line.split(" ", 1) 997 | cmd.extend((opt, rest)) 998 | else: 999 | cmd.append(line) 1000 | else: 1001 | cmd.append(line) 1002 | return parse_args(cmd) 1003 | except IOError: 1004 | return {} 1005 | 1006 | 1007 | def parse_args(cmd=None, ignore_defaults=False): 1008 | "Parse terminal-style args list, or sys.argv[1:] if no argument is passed." 1009 | parser = argparse.ArgumentParser( 1010 | description="Usage: droopy [options] [PORT]", 1011 | epilog='Example:\n droopy -m "Hi, this is Bob. You can send me a file." -p avatar.png' 1012 | ) 1013 | parser.add_argument("port", type=int, nargs='?', default=8000, 1014 | help='port number to host droopy upon') 1015 | parser.add_argument('-d', '--directory', type=str, default='.', 1016 | help='set the directory to upload files to') 1017 | parser.add_argument('-m', '--message', type=str, default='', 1018 | help='set the message') 1019 | parser.add_argument('-p', '--picture', type=str, default='', 1020 | help='set the picture') 1021 | parser.add_argument('--publish-files', '--dl', action='store_true', default=False, 1022 | help='provide download links') 1023 | parser.add_argument('-a', '--auth', type=str, default='', 1024 | help='set the authentication credentials, in form USER:PASS') 1025 | parser.add_argument('--ssl', type=str, default='', 1026 | help='set up https using the certificate file') 1027 | parser.add_argument('--chmod', type=str, default=None, 1028 | help='set the file permissions (octal value)') 1029 | parser.add_argument('--save-config', action='store_true', default=False, 1030 | help='save options in a configuration file') 1031 | parser.add_argument('--delete-config', action='store_true', default=False, 1032 | help='delete the configuration file and exit') 1033 | parser.add_argument('--config-file', default=default_configfile(), 1034 | help='configuration file to load terminal arguments from.') 1035 | args = parser.parse_args(cmd) 1036 | if args.picture: 1037 | if os.path.exists(args.picture): 1038 | args.picture = fullpath(args.picture) 1039 | else: 1040 | print("Picture not found: '{0}'".format(args.picture)) 1041 | if args.delete_config: 1042 | filename = default_configfile() 1043 | os.remove(filename) 1044 | print('Deleted ' + filename) 1045 | sys.exit(0) 1046 | if args.auth: 1047 | if ':' not in args.auth: 1048 | print("Error: authentication credentials must be " 1049 | "specified as USER:PASSWORD") 1050 | sys.exit(1) 1051 | if args.ssl: 1052 | if not os.path.isfile(args.ssl): 1053 | print("PEM file not found: '{0}'".format(args.ssl)) 1054 | sys.exit(1) 1055 | args.ssl = fullpath(args.ssl) 1056 | if args.chmod is not None: 1057 | try: 1058 | args.chmod = int(args.chmod, 8) 1059 | except ValueError: 1060 | print("Invalid octal value passed to chmod option: '{0}'".format(args.chmod)) 1061 | sys.exit(1) 1062 | # Needs to be set after de-defaulting because CWD varies, obviously. :) 1063 | args.directory = fullpath(args.directory) 1064 | d_args = vars(args) 1065 | if ignore_defaults: 1066 | default_set = parse_args([]) 1067 | for k, v in default_set.items(): 1068 | if v == d_args[k]: 1069 | del d_args[k] 1070 | return d_args 1071 | 1072 | 1073 | def main(): 1074 | "Encapsulating main prevents scope leakage and pleases linters." 1075 | print('''\ 1076 | _____ 1077 | | \.----.-----.-----.-----.--.--. 1078 | | -- | _| _ | _ | _ | | | 1079 | |_____/|__| |_____|_____| __|___ | 1080 | |__| |_____| 1081 | ''') 1082 | term_args = parse_args(ignore_defaults=True) 1083 | cfg = term_args.get('config_file', default_configfile()) 1084 | args = load_options(cfg) 1085 | if args: 1086 | print("Configuration found in {0}".format(cfg)) 1087 | args.update(term_args) 1088 | else: 1089 | print("No configuration file found") 1090 | args.update(parse_args(ignore_defaults=False)) 1091 | if args['save_config']: 1092 | cfg = args.get('config_file', default_configfile()) 1093 | save_options(cfg) 1094 | print("Options saved in {0}".format(cfg)) 1095 | print("Files will be uploaded to {0}\n".format(args['directory'])) 1096 | proto = 'https' if args['ssl'] else 'http' 1097 | print("HTTP server starting...", 1098 | "Check it out at {0}://localhost:{1}".format(proto, args['port'])) 1099 | try: 1100 | run(port=args['port'], 1101 | certfile=args['ssl'], 1102 | picture=args['picture'], 1103 | message=args['message'], 1104 | directory=args['directory'], 1105 | file_mode=args['chmod'], 1106 | publish_files=args['publish_files'], 1107 | auth=args['auth'], 1108 | templates=default_templates, 1109 | localisations=default_localisations) 1110 | except KeyboardInterrupt: 1111 | print('^C received, awaiting termination of remaining server threads..') 1112 | 1113 | if __name__ == '__main__': 1114 | main() 1115 | -------------------------------------------------------------------------------- /img/droopy-in-browser-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackp/Droopy/6be2d15098c72c2873bd8978371405e108a56a8b/img/droopy-in-browser-thumb.png -------------------------------------------------------------------------------- /img/droopy-in-terminal-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackp/Droopy/6be2d15098c72c2873bd8978371405e108a56a8b/img/droopy-in-terminal-thumb.png -------------------------------------------------------------------------------- /man/droopy.1: -------------------------------------------------------------------------------- 1 | .\" Manpage for droopy. 2 | .\" Contact stackp@online.fr to correct errors or typos. 3 | .TH man 1 "21 November 2013" "20131121" "droopy man page" 4 | .SH NAME 5 | droopy \- mini Web server to let others upload files to your computer. 6 | .SH SYNOPSIS 7 | droopy [options] [PORT] 8 | .SH DESCRIPTION 9 | Droopy is a mini Web server whose sole purpose is to let others upload files to your computer. 10 | .TP 5 11 | The default port is 8000. 12 | .SH OPTIONS 13 | .TP 5 14 | \fB\-h, \-\-help\fP 15 | .br 16 | Show help message and exit. 17 | .TP 5 18 | \fB\-d DIRECTORY, \-\-directory DIRECTORY\fP 19 | .br 20 | Set the directory to upload files to. 21 | .TP 5 22 | \fB\-m MESSAGE, \-\-message MESSAGE\fP 23 | .br 24 | Set the message. 25 | .TP 5 26 | \fB\-p PICTURE, \-\-picture PICTURE\fP 27 | .br 28 | Set the picture. 29 | .TP 5 30 | \fB\-\-dl\fP 31 | .br 32 | Provide download links. 33 | .TP 5 34 | \fB\-a USER:PASS, \-\-auth USER:PASS\fP 35 | .br 36 | Set the authentication credentials. 37 | .TP 5 38 | \fB\-\-ssl PEMFILE\fP 39 | .br 40 | Set up https using the certificate file. 41 | .TP 5 42 | \fB\-\-chmod MODE\fP 43 | .br 44 | set the file permissions (octal value). 45 | .TP 5 46 | \fB\-\-save-config\fP 47 | .br 48 | Save options in a configuration file. 49 | .TP 5 50 | \fB\-\-delete-config\fP 51 | .br 52 | Delete the configuration file and exit. 53 | .SH EXAMPLE 54 | droopy \-m "Hi, this is Bob. You can send me a file." \-p avatar.png 55 | .SH AUTHOR 56 | Pierre Duquesne 57 | --------------------------------------------------------------------------------