commit 8aab8b6da05ecfa48a02e50cc56639f5dffcca5e Author: jeffrey Date: Sun Jul 13 10:48:11 2025 +0200 Ajouter droopy diff --git a/droopy b/droopy new file mode 100644 index 0000000..706a006 --- /dev/null +++ b/droopy @@ -0,0 +1,1121 @@ +#! /usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Droopy (http://stackp.online.fr/droopy) +Copyright 2008-2013 (c) Pierre Duquesne +Licensed under the New BSD License. + +Changelog + 20151025 * Global variables removed + * Code refactoring and re-layout + * Python 2 and 3 compatibility + * Efficiency and Security improvements + * Added --config-file option. + * Retains backwards compatibility. + 20131121 * Update HTML/CSS for mobile devices + * Add HTTPS support + * Add HTTP basic authentication + * Add option to change uploaded file permissions + * Add support for HTML5 multiple file upload + 20120108 * Taiwanese translation by Li-cheng Hsu. + 20110928 * Correctly save message with --save-config. Fix by Sven Radde. + 20110708 * Polish translation by Jacek Politowski. + 20110625 * Fix bug regarding filesystem name encoding. + * Save the --dl option when --save-config is passed. + 20110501 * Add the --dl option to let clients download files. + * CSS speech bubble. + 20101130 * CSS and HTML update. Switch to the new BSD License. + 20100523 * Simplified Chinese translation by Ye Wei. + 20100521 * Hungarian translation by Csaba Szigetvári. + * Russian translation by muromec. + * Use %APPDATA% Windows environment variable -- fix by Maik. + 20091229 * Brazilian Portuguese translation by + Carlos Eduardo Moreira dos Santos and Toony Poony. + * IE layout fix by Carlos Eduardo Moreira dos Santos. + * Galician translation by Miguel Anxo Bouzada. + 20090721 * Indonesian translation by Kemas. + 20090205 * Japanese translation by Satoru Matsumoto. + * Slovak translation by CyberBoBaK. + 20090203 * Norwegian translation by Preben Olav Pedersen. + 20090202 * Korean translation by xissy. + * Fix for unicode filenames by xissy. + * Relies on 127.0.0.1 instead of "localhost" hostname. + 20090129 * Serbian translation by kotnik. + 20090125 * Danish translation by jan. + 20081210 * Greek translation by n2j3. + 20081128 * Slovene translation by david. + * Romanian translation by Licaon. + 20081022 * Swedish translation by David Eurenius. + 20081001 * Droopy gets pretty (css and html rework). + * Finnish translation by ipppe. + 20080926 * Configuration saving and loading. + 20080906 * Extract the file base name (some browsers send the full path). + 20080905 * File is uploaded directly into the specified directory. + 20080904 * Arabic translation by Djalel Chefrour. + * Italian translation by fabius and d1s4st3r. + * Dutch translation by Tonio Voerman. + * Portuguese translation by Pedro Palma. + * Turkish translation by Heartsmagic. + 20080727 * Spanish translation by Federico Kereki. + 20080624 * Option -d or --directory to specify the upload directory. + 20080622 * File numbering to avoid overwriting. + 20080620 * Czech translation by Jiří. + * German translation by Michael. + 20080408 * First release. +""" +from __future__ import print_function +import sys +if sys.version_info >= (3, 0): + from http import server as httpserver + import socketserver + from urllib import parse as urllibparse + unicode = str +else: + import BaseHTTPServer as httpserver + import SocketServer as socketserver + import urllib as urllibparse + +import cgi +import os +import posixpath +import ntpath +import argparse +import mimetypes +import shutil +import tempfile +import socket +import base64 +import functools +import datetime + +def _decode_str_if_py2(inputstr, encoding='utf-8'): + "Will return decoded with given encoding *if* input is a string and it's Py2." + if sys.version_info < (3,) and isinstance(inputstr, str): + return inputstr.decode(encoding) + else: + return inputstr + +def _encode_str_if_py2(inputstr, encoding='utf-8'): + "Will return encoded with given encoding *if* input is a string and it's Py2" + if sys.version_info < (3,) and isinstance(inputstr, str): + return inputstr.encode(encoding) + else: + return inputstr + +def fullpath(path): + "Shortcut for os.path abspath(expanduser())" + return os.path.abspath(os.path.expanduser(path)) + +def basename(path): + "Extract the file base name (some browsers send the full file path)." + for mod in posixpath, ntpath: + path = mod.basename(path) + return path + +def check_auth(method): + "Wraps methods on the request handler to require simple auth checks." + def decorated(self, *pargs): + "Reject if auth fails." + if self.auth: + # TODO: Between minor versions this handles str/bytes differently + received = self.get_case_insensitive_header('Authorization', None) + expected = 'Basic ' + base64.b64encode(self.auth.encode("utf-8")).decode("utf-8") + # TODO: Timing attack? + if received != expected: + self.send_response(401) + self.send_header('WWW-Authenticate', 'Basic realm=\"Droopy\"') + self.send_header('Content-type', 'text/html') + self.end_headers() + else: + method(self, *pargs) + else: + method(self, *pargs) + functools.update_wrapper(decorated, method) + return decorated + + +class Abort(Exception): + "Used by handle to rethrow exceptions in ThreadedHTTPServer." + + +class DroopyFieldStorage(cgi.FieldStorage): + """ + The file is created in the destination directory and its name is + stored in the tmpfilename attribute. + + Adds a keyword-argument "directory", which is where files are to be + stored. Because of CGI magic this might not be thread-safe. + """ + + TMPPREFIX = 'tmpdroopy' + + # Would love to do a **kwargs job here but cgi has some recursive + # magic that passes all possible arguments positionally.. + def __init__(self, fp=None, headers=None, outerboundary=b'', + environ=os.environ, keep_blank_values=0, strict_parsing=0, + limit=None, encoding='utf-8', errors='replace', + max_num_fields=None, separator='&', directory='.'): + """ + Adds 'directory' argument to FieldStorage.__init__. + Retains compatibility with FieldStorage.__init__ (which involves magic) + """ + self.directory = directory + # Not only is cgi.FieldStorage full of magic, it's DIFFERENT + # magic in Py2/Py3. Here's a case of the core library making + # life difficult, in a class that's *supposed to be subclassed*! + if sys.version_info >= (3, 9): + cgi.FieldStorage.__init__(self, fp, headers, outerboundary, + environ, keep_blank_values, + strict_parsing, limit, encoding, errors, + max_num_fields, separator) + else: + cgi.FieldStorage.__init__(self, fp, headers, outerboundary, + environ, keep_blank_values, + strict_parsing, limit, encoding, errors) + + # Binary is passed in Py2 but not Py3. + def make_file(self, binary=None): + "Overrides builtin method to store tempfile in the set directory." + fd, name = tempfile.mkstemp(dir=self.directory, prefix=self.TMPPREFIX) + # Pylint doesn't like these if they're not declared in __init__ first, + # but setting tmpfile there leads to odd errors where it's never re-set + # to a file descriptor. + self.tmpfile = os.fdopen(fd, 'w+b') + self.tmpfilename = name + return self.tmpfile + + +class HTTPUploadHandler(httpserver.BaseHTTPRequestHandler): + "The guts of Droopy-a custom handler that accepts files & serves templates" + + @property + def templates(self): + "Ensure provided." + raise NotImplementedError("Must set class with a templates dict!") + + @property + def localisations(self): + "Ensure provided." + raise NotImplementedError("Must set class with a localisations dict!") + + @property + def directory(self): + "Ensure provided." + raise NotImplementedError("Must provide directory to host.") + + message = '' + picture = '' + publish_files = False + file_mode = None + protocol_version = 'HTTP/1.0' + form_field = 'upfile' + auth = '' + certfile = None + divpicture = '
' + + def get_case_insensitive_header(self, hdrname, default): + "Python 2 and 3 differ in header capitalisation!" + lc_hdrname = hdrname.lower() + lc_headers = dict((h.lower(), h) for h in self.headers.keys()) + if lc_hdrname in lc_headers: + return self.headers[lc_headers[lc_hdrname]] + else: + return default + + @staticmethod + def prefcode_tuple(prefcode): + "Parse language preferences into (preference, language) tuples." + prefbits = prefcode.split(";q=") + if len(prefbits) == 1: + return (1, prefbits[0]) + else: + return (float(prefbits[1]), prefbits[0]) + + def parse_accepted_languages(self): + "Parse accept-language header" + lhdr = self.get_case_insensitive_header('accept-language', default='') + if lhdr: + accepted = [self.prefcode_tuple(lang) for lang in lhdr.split(',')] + accepted.sort() + accepted.reverse() + return [x[1] for x in accepted] + else: + return [] + + def choose_language(self): + "Choose localisation based on accept-language header (default 'en')" + accepted = self.parse_accepted_languages() + # -- Choose the appropriate translation dictionary (default is english) + lang = "en" + for alang in accepted: + if alang in self.localisations: + lang = alang + break + return self.localisations[lang] + + def html(self, page): + """ + page can be "main", "success", or "error" + returns an html page (in the appropriate language) as a string + """ + dico = {} + dico.update(self.choose_language()) + # -- Set message and picture + if self.message: + dico['message'] = '
{0}
'.format(self.message) + else: + dico["message"] = '' + # The default appears to be missing/broken, so needs a bit of love anyway. + if self.picture: + dico["divpicture"] = self.divpicture + else: + dico["divpicture"] = '' + # -- Possibly provide download links + # TODO: Sanity-check for injections + links = '' + if self.publish_files: + for name in self.published_files(): + encoded_name = urllibparse.quote(_encode_str_if_py2(name)) + + # JEFFREY PATCH: Ajout de la datetime d'upload dans le nom affiché + stat_info = os.stat(name) + timestamp = getattr(stat_info, 'st_birthtime', stat_info.st_ctime) + dt_object = datetime.datetime.fromtimestamp(timestamp) + mytime = dt_object.strftime("%d/%m/%Y at %H:%M:%S") + name = f'{name} (uploaded: {mytime})' + links += '{1}'.format(encoded_name, name) + links = '
' + links + '
' + dico["files"] = links + # -- Add a link to discover the url + if self.client_address[0] == "127.0.0.1": + dico["port"] = self.server.server_port + dico["ssl"] = int(self.certfile is not None) + dico["linkurl"] = self.templates['linkurl'] % dico + else: + dico["linkurl"] = '' + return self.templates[page] % dico + + @check_auth + def do_GET(self): + "Standard method to override in this Server object." + name = self.path.lstrip('/') + name = urllibparse.unquote(name) + name = _decode_str_if_py2(name, 'utf-8') + + # TODO: Refactor special-method handling to make more modular? + # Include ability to self-define "special method" prefix path? + if self.picture != None and self.path == '/__droopy/picture': + # send the picture + self.send_file(self.picture) + # TODO Verify that this is path-injection proof + elif name in self.published_files(): + localpath = os.path.join(self.directory, name) + self.send_file(localpath) + else: + self.send_html(self.html("main")) + + @check_auth + def do_POST(self): + "Standard method to override in this Server object." + try: + self.log_message("Started file transfer") + # -- Save file (numbered to avoid overwriting, ex: foo-3.png) + form = DroopyFieldStorage(fp=self.rfile, + directory=self.directory, + headers=self.headers, + environ={'REQUEST_METHOD': self.command}) + file_items = form[self.form_field] + #-- Handle multiple file upload + if not isinstance(file_items, list): + file_items = [file_items] + for item in file_items: + filename = _decode_str_if_py2(basename(item.filename), "utf-8") + if filename == "": + continue + localpath = _encode_str_if_py2(os.path.join(self.directory, filename), "utf-8") + root, ext = os.path.splitext(localpath) + + # JEFFREY PATCH: forcer l'écrasement d'un fichier existant. + if os.path.exists(localpath): + os.remove(localpath) + + if hasattr(item, 'tmpfile'): + # DroopyFieldStorage.make_file() has been called + item.tmpfile.close() + shutil.move(item.tmpfilename, localpath) + else: + # no temporary file, self.file is a StringIO() + # see cgi.FieldStorage.read_lines() + with open(localpath, "wb") as fout: + shutil.copyfileobj(item.file, fout) + if self.file_mode is not None: + os.chmod(localpath, self.file_mode) + self.log_message("Received: %s", os.path.basename(localpath)) + + # -- Reply + if self.publish_files: + # The file list gives a feedback for the upload success + self.send_resp_headers(301, {'Location': '/'}, end=True) + else: + self.send_html(self.html("success")) + + except Exception as e: + self.log_message(repr(e)) + self.send_html(self.html("error")) + # raise e # Dev only + + def send_resp_headers(self, response_code, headers_dict, end=False): + "Just a shortcut for a common operation." + self.send_response(response_code) + for k, v in headers_dict.items(): + self.send_header(k, v) + if end: + self.end_headers() + + def send_html(self, htmlstr): + "Simply returns htmlstr with the appropriate content-type/status." + self.send_resp_headers(200, {'Content-type': 'text/html; charset=utf-8'}, end=True) + self.wfile.write(htmlstr.encode("utf-8")) + + def send_file(self, localpath): + "Does what it says on the tin! Includes correct content-type/length." + with open(localpath, 'rb') as f: + self.send_resp_headers(200, + {'Content-length': os.fstat(f.fileno())[6], + 'Content-type': mimetypes.guess_type(localpath)[0]}, + end=True) + shutil.copyfileobj(f, self.wfile) + + def published_files(self): + "Returns the list of files that should appear as download links." + names = [] + # In py2, listdir() returns strings when the directory is a string. + for name in os.listdir(unicode(self.directory)): + if name.startswith(DroopyFieldStorage.TMPPREFIX): + continue + npath = os.path.join(self.directory, name) + if os.path.isfile(npath): + names.append(name) + names.sort(key=lambda s: s.lower()) + return names + + def handle(self): + "Lets parent object handle, but redirects socket exceptions as 'Abort's." + try: + httpserver.BaseHTTPRequestHandler.handle(self) + except socket.error as e: + self.log_message(str(e)) + raise Abort(str(e)) + + +class ThreadedHTTPServer(socketserver.ThreadingMixIn, + httpserver.HTTPServer): + "Allows propagation of socket.error in HTTPUploadHandler.handle" + def handle_error(self, request, client_address): + "Override socketserver.handle_error" + exctype = sys.exc_info()[0] + if not exctype is Abort: + httpserver.HTTPServer.handle_error(self, request, client_address) + + +def run(hostname='', + port=80, + templates=None, + localisations=None, + directory='.', + timeout=3*60, + picture=None, + message='', + file_mode=None, + publish_files=False, + auth='', + certfile=None, + permitted_ciphers=( + 'ECDH+AESGCM:ECDH+AES256:ECDH+AES128:ECDH+3DES' + ':RSA+AESGCM:RSA+AES:RSA+3DES' + ':!aNULL:!MD5:!DSS')): + """ + certfile should be the path of a PEM TLS certificate. + + permitted_ciphers, if a TLS cert is provided, is an OpenSSL cipher string. + The default here is taken from: + https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ + ..with DH-only ciphers removed because of precomputation hazard. + """ + if templates is None or localisations is None: + raise ValueError("Must provide templates *and* localisations.") + socket.setdefaulttimeout(timeout) + HTTPUploadHandler.templates = templates + HTTPUploadHandler.directory = directory + HTTPUploadHandler.localisations = localisations + HTTPUploadHandler.certfile = certfile + HTTPUploadHandler.publish_files = publish_files + HTTPUploadHandler.picture = picture + HTTPUploadHandler.message = message + HTTPUploadHandler.file_mode = file_mode + HTTPUploadHandler.auth = auth + httpd = ThreadedHTTPServer((hostname, port), HTTPUploadHandler) + # TODO: Specify TLS1.2 only? + if certfile: + try: + import ssl + except: + print("Error: Could not import module 'ssl', exiting.") + sys.exit(2) + httpd.socket = ssl.wrap_socket( + httpd.socket, + certfile=certfile, + ciphers=permitted_ciphers, + server_side=True) + httpd.serve_forever() + +# -- Dato + +# -- HTML templates + +style = ''' + +''' + +userinfo = ''' +
+ %(message)s + %(divpicture)s +
+''' + +maintmpl = ''' + + + +%(maintitle)s +''' + style + ''' + + +%(linkurl)s +
+
+
+
+ + +
+
+
+ + + +
+ +
+
%(sending)s
+
+
+''' + userinfo + ''' +%(files)s +
+ + +''' + +successtmpl = ''' + + + %(successtitle)s +''' + style + ''' + + +
+
+
+ %(received)s + %(another)s +
+
+''' + userinfo + ''' +
+ + +''' + +errortmpl = ''' + + + %(errortitle)s +''' + style + ''' + + +
+
+
+ %(problem)s + %(retry)s +
+
+''' + userinfo + ''' +
+ + +''' + +linkurltmpl = '''''' + + +default_templates = { + "main": maintmpl, + "success": successtmpl, + "error": errortmpl, + "linkurl": linkurltmpl} + +# -- Translations +default_localisations = { + 'ar' : { + "maintitle": u"إرسال ملف", + "submit": u"إرسال", + "sending": u"الملف قيد الإرسال", + "successtitle": u"تم استقبال الملف", + "received": u"تم استقبال الملف !", + "another": u"إرسال ملف آخر", + "errortitle": u"مشكلة", + "problem": u"حدثت مشكلة !", + "retry": u"إعادة المحاولة", + "discover": u"اكتشاف عنوان هذه الصفحة"}, + 'cs' : { + "maintitle": u"Poslat soubor", + "submit": u"Poslat", + "sending": u"Posílám", + "successtitle": u"Soubor doručen", + "received": u"Soubor doručen !", + "another": u"Poslat další soubor", + "errortitle": u"Chyba", + "problem": u"Stala se chyba !", + "retry": u"Zkusit znova.", + "discover": u"Zjistit adresu stránky"}, + 'da' : { + "maintitle": u"Send en fil", + "submit": u"Send", + "sending": u"Sender", + "successtitle": u"Fil modtaget", + "received": u"Fil modtaget!", + "another": u"Send en fil til.", + "errortitle": u"Problem", + "problem": u"Det er opstået en fejl!", + "retry": u"Forsøg igen.", + "discover": u"Find adressen til denne side"}, + 'de' : { + "maintitle": "Datei senden", + "submit": "Senden", + "sending": "Sendet", + "successtitle": "Datei empfangen", + "received": "Datei empfangen!", + "another": "Weitere Datei senden", + "errortitle": "Fehler", + "problem": "Ein Fehler ist aufgetreten!", + "retry": "Wiederholen", + "discover": "Internet-Adresse dieser Seite feststellen"}, + 'el' : { + "maintitle": u"Στείλε ένα αρχείο", + "submit": u"Αποστολή", + "sending": u"Αποστέλλεται...", + "successtitle": u"Επιτυχής λήψη αρχείου ", + "received": u"Λήψη αρχείου ολοκληρώθηκε", + "another": u"Στείλε άλλο ένα αρχείο", + "errortitle": u"Σφάλμα", + "problem": u"Παρουσιάστηκε σφάλμα", + "retry": u"Επανάληψη", + "discover": u"Βρες την διεύθυνση της σελίδας"}, + 'en' : { + "maintitle": "Send a file", + "submit": "Send", + "sending": "Sending", + "successtitle": "File received", + "received": "File received!", + "another": "Send another file.", + "errortitle": "Problem", + "problem": "There has been a problem!", + "retry": "Retry.", + "discover": "Discover the address of this page"}, + 'es' : { + "maintitle": u"Enviar un archivo", + "submit": u"Enviar", + "sending": u"Enviando", + "successtitle": u"Archivo recibido", + "received": u"¡Archivo recibido!", + "another": u"Enviar otro archivo.", + "errortitle": u"Error", + "problem": u"¡Hubo un problema!", + "retry": u"Reintentar", + "discover": u"Descubrir la dirección de esta página"}, + 'fi' : { + "maintitle": u"Lähetä tiedosto", + "submit": u"Lähetä", + "sending": u"Lähettää", + "successtitle": u"Tiedosto vastaanotettu", + "received": u"Tiedosto vastaanotettu!", + "another": u"Lähetä toinen tiedosto.", + "errortitle": u"Virhe", + "problem": u"Virhe lahetettäessä tiedostoa!", + "retry": u"Uudelleen.", + "discover": u"Näytä tämän sivun osoite"}, + 'fr' : { + "maintitle": u"Envoyer un fichier", + "submit": u"Envoyer", + "sending": u"Envoi en cours", + "successtitle": u"Fichier reçu", + "received": u"Fichier reçu !", + "another": u"Envoyer un autre fichier.", + "errortitle": u"Problème", + "problem": u"Il y a eu un problème !", + "retry": u"Réessayer.", + "discover": u"Découvrir l'adresse de cette page"}, + 'gl' : { + "maintitle": u"Enviar un ficheiro", + "submit": u"Enviar", + "sending": u"Enviando", + "successtitle": u"Ficheiro recibido", + "received": u"Ficheiro recibido!", + "another": u"Enviar outro ficheiro.", + "errortitle": u"Erro", + "problem": u"Xurdíu un problema!", + "retry": u"Reintentar", + "discover": u"Descubrir o enderezo desta páxina"}, + 'hu' : { + "maintitle": u"Állomány küldése", + "submit": u"Küldés", + "sending": u"Küldés folyamatban", + "successtitle": u"Az állomány beérkezett", + "received": u"Az állomány beérkezett!", + "another": u"További állományok küldése", + "errortitle": u"Hiba", + "problem": u"Egy hiba lépett fel!", + "retry": u"Megismételni", + "discover": u"Az oldal Internet-címének megállapítása"}, + 'id' : { + "maintitle": "Kirim sebuah berkas", + "submit": "Kirim", + "sending": "Mengirim", + "successtitle": "Berkas diterima", + "received": "Berkas diterima!", + "another": "Kirim berkas yang lain.", + "errortitle": "Permasalahan", + "problem": "Telah ditemukan sebuah kesalahan!", + "retry": "Coba kembali.", + "discover": "Kenali alamat IP dari halaman ini"}, + 'it' : { + "maintitle": u"Invia un file", + "submit": u"Invia", + "sending": u"Invio in corso", + "successtitle": u"File ricevuto", + "received": u"File ricevuto!", + "another": u"Invia un altro file.", + "errortitle": u"Errore", + "problem": u"Si è verificato un errore!", + "retry": u"Riprova.", + "discover": u"Scopri l’indirizzo di questa pagina"}, + 'ja' : { + "maintitle": u"ファイル送信", + "submit": u"送信", + "sending": u"送信中", + "successtitle": u"受信完了", + "received": u"ファイルを受信しました!", + "another": u"他のファイルを送信する", + "errortitle": u"問題発生", + "problem": u"問題が発生しました!", + "retry": u"リトライ", + "discover": u"このページのアドレスを確認する"}, + 'ko' : { + "maintitle": u"파일 보내기", + "submit": u"보내기", + "sending": u"보내는 중", + "successtitle": u"파일이 받아졌습니다", + "received": u"파일이 받아졌습니다!", + "another": u"다른 파일 보내기", + "errortitle": u"문제가 발생했습니다", + "problem": u"문제가 발생했습니다!", + "retry": u"다시 시도", + "discover": u"이 페이지 주소 알아보기"}, + 'nl' : { + "maintitle": "Verstuur een bestand", + "submit": "Verstuur", + "sending": "Bezig met versturen", + "successtitle": "Bestand ontvangen", + "received": "Bestand ontvangen!", + "another": "Verstuur nog een bestand.", + "errortitle": "Fout", + "problem": "Er is een fout opgetreden!", + "retry": "Nog eens.", + "discover": "Vind het adres van deze pagina"}, + 'no' : { + "maintitle": u"Send en fil", + "submit": u"Send", + "sending": u"Sender", + "successtitle": u"Fil mottatt", + "received": u"Fil mottatt !", + "another": u"Send en ny fil.", + "errortitle": u"Feil", + "problem": u"Det har skjedd en feil !", + "retry": u"Send på nytt.", + "discover": u"Finn addressen til denne siden"}, + 'pl' : { + "maintitle": u"Wyślij plik", + "submit": u"Wyślij", + "sending": u"Wysyłanie", + "successtitle": u"Plik wysłany", + "received": u"Plik wysłany!", + "another": u"Wyślij kolejny plik.", + "errortitle": u"Problem", + "problem": u"Wystąpił błąd!", + "retry": u"Spróbuj ponownie.", + "discover": u"Znajdź adres tej strony"}, + 'pt' : { + "maintitle": u"Enviar um ficheiro", + "submit": u"Enviar", + "sending": u"A enviar", + "successtitle": u"Ficheiro recebido", + "received": u"Ficheiro recebido !", + "another": u"Enviar outro ficheiro.", + "errortitle": u"Erro", + "problem": u"Ocorreu um erro !", + "retry": u"Tentar novamente.", + "discover": u"Descobrir o endereço desta página"}, + 'pt-br' : { + "maintitle": u"Enviar um arquivo", + "submit": u"Enviar", + "sending": u"Enviando", + "successtitle": u"Arquivo recebido", + "received": u"Arquivo recebido!", + "another": u"Enviar outro arquivo.", + "errortitle": u"Erro", + "problem": u"Ocorreu um erro!", + "retry": u"Tentar novamente.", + "discover": u"Descobrir o endereço desta página"}, + 'ro' : { + "maintitle": u"Trimite un fişier", + "submit": u"Trimite", + "sending": u"Se trimite", + "successtitle": u"Fişier recepţionat", + "received": u"Fişier recepţionat !", + "another": u"Trimite un alt fişier.", + "errortitle": u"Problemă", + "problem": u"A intervenit o problemă !", + "retry": u"Reîncearcă.", + "discover": u"Descoperă adresa acestei pagini"}, + 'ru' : { + "maintitle": u"Отправить файл", + "submit": u"Отправить", + "sending": u"Отправляю", + "successtitle": u"Файл получен", + "received": u"Файл получен !", + "another": u"Отправить другой файл.", + "errortitle": u"Ошибка", + "problem": u"Произошла ошибка !", + "retry": u"Повторить.", + "discover": u"Посмотреть адрес этой страницы"}, + 'sk' : { + "maintitle": u"Pošli súbor", + "submit": u"Pošli", + "sending": u"Posielam", + "successtitle": u"Súbor prijatý", + "received": u"Súbor prijatý !", + "another": u"Poslať ďalší súbor.", + "errortitle": u"Chyba", + "problem": u"Vyskytla sa chyba!", + "retry": u"Skúsiť znova.", + "discover": u"Zisti adresu tejto stránky"}, + 'sl' : { + "maintitle": u"Pošlji datoteko", + "submit": u"Pošlji", + "sending": u"Pošiljam", + "successtitle": u"Datoteka prejeta", + "received": u"Datoteka prejeta !", + "another": u"Pošlji novo datoteko.", + "errortitle": u"Napaka", + "problem": u"Prišlo je do napake !", + "retry": u"Poizkusi ponovno.", + "discover": u"Poišči naslov na tej strani"}, + 'sr' : { + "maintitle": u"Pošalji fajl", + "submit": u"Pošalji", + "sending": u"Šaljem", + "successtitle": u"Fajl primljen", + "received": u"Fajl primljen !", + "another": u"Pošalji još jedan fajl.", + "errortitle": u"Problem", + "problem": u"Desio se problem !", + "retry": u"Pokušaj ponovo.", + "discover": u"Otkrij adresu ove stranice"}, + 'sv' : { + "maintitle": u"Skicka en fil", + "submit": u"Skicka", + "sending": u"Skickar...", + "successtitle": u"Fil mottagen", + "received": u"Fil mottagen !", + "another": u"Skicka en fil till.", + "errortitle": u"Fel", + "problem": u"Det har uppstått ett fel !", + "retry": u"Försök igen.", + "discover": u"Ta reda på adressen till denna sida"}, + 'tr' : { + "maintitle": u"Dosya gönder", + "submit": u"Gönder", + "sending": u"Gönderiliyor...", + "successtitle": u"Gönderildi", + "received": u"Gönderildi", + "another": u"Başka bir dosya gönder.", + "errortitle": u"Problem.", + "problem": u"Bir problem oldu !", + "retry": u"Yeniden dene.", + "discover": u"Bu sayfanın adresini bul"}, + 'zh-cn' : { + "maintitle": u"发送文件", + "submit": u"发送", + "sending": u"发送中", + "successtitle": u"文件已收到", + "received": u"文件已收到!", + "another": u"发送另一个文件。", + "errortitle": u"问题", + "problem": u"出现问题!", + "retry": u"重试。", + "discover": u"查看本页面的地址"}, + 'zh-tw' : { + "maintitle": u"上傳檔案", + "submit": u"上傳", + "sending": u"傳送中...", + "successtitle": u"已收到檔案", + "received": u"已收到檔案!", + "another": u"上傳另一個檔案。", + "errortitle": u"錯誤", + "problem": u"出現錯誤!", + "retry": u"重試。", + "discover": u"查閱本網頁的網址"} +} # Ends default_localisations dictionary. + + +# -- Options + +def default_configfile(): + "Returns appropriate absolute path to configfile, per-platform." + appname = 'droopy' + if os.name == 'posix': + filename = os.path.join(os.environ['HOME'], "." + appname) + elif os.name == 'mac': + filename = os.path.join(os.environ['HOME'], 'Library', 'Application Support', appname) + elif os.name == 'nt': + filename = os.path.join(os.environ['APPDATA'], appname) + else: + # Exaggerated shrug + filename = './' + appname + return filename + + +def save_options(cfg): + "Dumps sys.argv with one argument per line." + with open(cfg, "w") as O: + ignorenext = False + for opt in sys.argv[1:]: + if ignorenext: + ignorenext = False + continue + if opt.startswith("-"): + if opt.strip() in ("--save-config", "--delete-config"): + continue + if opt.strip() == '--config-file': + ignorenext = True + continue + O.write("\n") + else: + O.write(" ") + O.write(opt) + + +def load_options(cfg_loc): + """ + Attempts to open location, piece lines back together into a terminal-style + invocation, and pass to parse_args. + """ + try: + with open(cfg_loc) as f: + cmd = [] + for line in f: + line = line.strip() + if not line: + continue + if line.startswith("-"): + if " " in line: + opt, rest = line.split(" ", 1) + cmd.extend((opt, rest)) + else: + cmd.append(line) + else: + cmd.append(line) + return parse_args(cmd) + except IOError: + return {} + + +def parse_args(cmd=None, ignore_defaults=False): + "Parse terminal-style args list, or sys.argv[1:] if no argument is passed." + parser = argparse.ArgumentParser( + description="Usage: droopy [options] [PORT]", + epilog='Example:\n droopy -m "Hi, this is Bob. You can send me a file." -p avatar.png' + ) + parser.add_argument("port", type=int, nargs='?', default=8000, + help='port number to host droopy upon') + parser.add_argument('-d', '--directory', type=str, default='.', + help='set the directory to upload files to') + parser.add_argument('-m', '--message', type=str, default='', + help='set the message') + parser.add_argument('-p', '--picture', type=str, default='', + help='set the picture') + parser.add_argument('--publish-files', '--dl', action='store_true', default=False, + help='provide download links') + parser.add_argument('-a', '--auth', type=str, default='', + help='set the authentication credentials, in form USER:PASS') + parser.add_argument('--ssl', type=str, default='', + help='set up https using the certificate file') + parser.add_argument('--chmod', type=str, default=None, + help='set the file permissions (octal value)') + parser.add_argument('--save-config', action='store_true', default=False, + help='save options in a configuration file') + parser.add_argument('--delete-config', action='store_true', default=False, + help='delete the configuration file and exit') + parser.add_argument('--config-file', default=default_configfile(), + help='configuration file to load terminal arguments from.') + args = parser.parse_args(cmd) + if args.picture: + if os.path.exists(args.picture): + args.picture = fullpath(args.picture) + else: + print("Picture not found: '{0}'".format(args.picture)) + if args.delete_config: + filename = default_configfile() + os.remove(filename) + print('Deleted ' + filename) + sys.exit(0) + if args.auth: + if ':' not in args.auth: + print("Error: authentication credentials must be " + "specified as USER:PASSWORD") + sys.exit(1) + if args.ssl: + if not os.path.isfile(args.ssl): + print("PEM file not found: '{0}'".format(args.ssl)) + sys.exit(1) + args.ssl = fullpath(args.ssl) + if args.chmod is not None: + try: + args.chmod = int(args.chmod, 8) + except ValueError: + print("Invalid octal value passed to chmod option: '{0}'".format(args.chmod)) + sys.exit(1) + # Needs to be set after de-defaulting because CWD varies, obviously. :) + args.directory = fullpath(args.directory) + d_args = vars(args) + if ignore_defaults: + default_set = parse_args([]) + for k, v in default_set.items(): + if v == d_args[k]: + del d_args[k] + return d_args + + +def main(): + "Encapsulating main prevents scope leakage and pleases linters." + print('''\ + _____ + | \.----.-----.-----.-----.--.--. + | -- | _| _ | _ | _ | | | + |_____/|__| |_____|_____| __|___ | + |__| |_____| + ''') + term_args = parse_args(ignore_defaults=True) + cfg = term_args.get('config_file', default_configfile()) + args = load_options(cfg) + if args: + print("Configuration found in {0}".format(cfg)) + args.update(term_args) + else: + print("No configuration file found") + args.update(parse_args(ignore_defaults=False)) + if args['save_config']: + cfg = args.get('config_file', default_configfile()) + save_options(cfg) + print("Options saved in {0}".format(cfg)) + print("Files will be uploaded to {0}\n".format(args['directory'])) + proto = 'https' if args['ssl'] else 'http' + print("HTTP server starting...", + "Check it out at {0}://localhost:{1}".format(proto, args['port'])) + try: + run(port=args['port'], + certfile=args['ssl'], + picture=args['picture'], + message=args['message'], + directory=args['directory'], + file_mode=args['chmod'], + publish_files=args['publish_files'], + auth=args['auth'], + templates=default_templates, + localisations=default_localisations) + except KeyboardInterrupt: + print('^C received, awaiting termination of remaining server threads..') + +if __name__ == '__main__': + main()