From 67d91a42e51325e631a48c3c8fa22909d996147c Mon Sep 17 00:00:00 2001 From: retoor Date: Tue, 26 Nov 2024 05:57:12 +0100 Subject: [PATCH] Updated readme. --- Makefile | 5 +- README.md | 30 ++++- src/rupload/__pycache__/app.cpython-312.pyc | Bin 4471 -> 11020 bytes src/rupload/__pycache__/cli.cpython-312.pyc | Bin 1640 -> 1855 bytes src/rupload/app.py | 138 ++++++++++++++++---- src/rupload/cli.py | 8 +- 6 files changed, 153 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 15dd724..58cdb9f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,7 @@ -all: ensure_env build serve +all: ensure_env clean build serve + +clean: + -@rm -rf src/rupload/__pycache__ ensure_env: -@python3 -m venv .venv diff --git a/README.md b/README.md index 0cb4672..8a1a39e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,30 @@ python3 -m venv .venv ``` ## Usage -``` -rupload.serve [host(127.0.0.1)] [port] [destination path for uploads] [max file size in bytes] -``` +`rupload.serve` [-h] + [--hostname HOSTNAME] + [--port PORT] + [--upload_folder UPLOAD_FOLDER] + [--upload_url UPLOAD_URL] + [--max_file_size MAX_FILE_SIZE] + +Start the file upload server. + +options: + -h, --help show this help message + and exit + --hostname HOSTNAME The hostname for the + server. + --port PORT The port to bind the + server to. + --upload_folder UPLOAD_FOLDER + Directory to store + uploaded files. + --upload_url UPLOAD_URL + HTTP(S) URL where the + server will serve the + uploaded files. + --max_file_size MAX_FILE_SIZE + Maximum file size in + bytes (default is + 50MB). diff --git a/src/rupload/__pycache__/app.cpython-312.pyc b/src/rupload/__pycache__/app.cpython-312.pyc index e8b797e812a0e1fb2438202ce18376cd8c5d4d3f..42295ff1091694883318ded62381af49238aad6b 100644 GIT binary patch literal 11020 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 delta 1866 zcmZWpU2GIp6h3!mc6Yje1zIWgciR$RRA{S4lDagMb|C>$8bAmoW*l~=-D#&g%blsT zbzuuOX`4W#8MP$(z?0Fm{v`N7!ka#tn4)W}BP1AK^sS*yKunD1&g`}(PIk`Td(XLN z&Uemt&h;}-T6TXIi-iH7_U_csx0L|DvrYL4loo4W&+k54&w<7d$IH6Se=WF3oR3=T z3PClv6k6nb4FOdgs8B1WVIZuAQAX6L8kr^o(Fr2O{$xgd5^?!U+J*F{FM!f@SZl=7 z8CU=f28ap++$;#ykT?A1 zIoy%*))vaEIX(|JVgQS!0|FBukv>UqBfeibr`Q=cHmZ!vgSw%~mOiPu0c$L$QCE~@ zJ*(TYjOK!+8G|$gH2_pZXKsZorwzrjWZ7b0b2_;Fy)4m_TS^h{E8k}376bcq3RdcN z%?d?v$4bKs?7#Mb7@G^-7kB(5)~=xiNCD@2ZfAiqPj0OAiL_=I7zCkFJhd3ggA_3V zW##ikh$-07(`k6Z@qxNkKtWk8J-r+9(ORcjN^=U;vbN=l+PH4nvN@tWhcSke&Algj zPM_;fCGJrE3W;YleJEphCOX>N-_E@5tO~zuv$b)1uVUy!+0H~7eQ4CV7r1xlpWs>E z`8#|pcFr85iNl!3WLu`gnU9=rH|@m;G&z{h|fQSvZMobGM4&e58lCT@76$Y@o1R5G#$A-Cl@r+-iN zuFJe#QC8*o#S%XVRDc~RpASc!sjBK)m0uFd%s%x$b*mcquX;aRzliVo&-qJ43ii1I z!{Yo_FTJ?6&-8)$s5Et-RP9!bjv2O&S+`vwiy1_zjB3t}#zZ?ShMh6=3$D=b5%G=w z<0ra%<-;eA_ohy|;;^Y_J%$FoT3ODc#?E?4bVF3TIHp;)8>E_|B3A>0x@M@f!Rc;l z*=sS%B}$J+6*rbqvMTc1kBh#72E+JTHaNGNb`>6lqBE0MCvTqlAyoIMYR5v?eAldq zwYH;-hITRMY?5@S-H(`P@z0t7S(R!oc2XW&U+^O5rSlDc4 zl`L_aHS&_r_f1wXQnqF6QA6VB^LW>drcI<~)|TywoF?%UM+eX*riql&+uwa8h5hsr zo_YP0@RJlRB}vb!+Bn^h+M8^~SlION2W#I4kvPNr@%gXbDMoshg`U5>=D$|oNYdrG zhPJ@^k$rXT6BG+zZvf5X+*%@K5SKY5>dSk^3q#x{IrAJ2KA{i6FfZP)c}ChMLv4QP zq>$gyVMSGCYL3~Od2*kS8_RvMTNxh@rKjL9$S+eP(#^Z zeIH+|69qyIZ@o2gx1kt5;w(t5h2^SKMd9>HBtCW28!I?Rf_^|cI^?C88&Y&LW7|1D znvh~mWYfAy6X?y2bmwx0o>pw#%=%NS1&?Vn(*;IHRGre-*a4%Zhrx^HGw>%E4z?@s zOiYfN>X@N*(G*&-j9PD@Smg;Jze4;W)S$%QdQc#Bv+4KDIddIwxf+M9wNtSbh)#uO zqF1A{Z~h2%KST3XsJd+83V7S9A+HKR|a)(P~0n2j!h5S20uG{)wwDrFbpL{*O@M3)76|SPm>siAX alxW$o? zUtE%xn44N8G1-7kSw{{iD+|QMmO!F`;SRTGw@rh~9WIdtBG(m-FDe=zFuCp=deJ%b z3Rl?VHnuQExyg6gRP0oMsu@Arbb!PMW=2NF+YJ178N}~0D1BlRVU+)*$INK?fro*G Yr=#i;v*b+{j#jr1%nU42MZ!Q00ATt}8vp @@ -28,7 +26,7 @@ UPLOAD_PAGE = """ display: flex; justify-content: center; align-items: center; - height: 100vh; + margin: 0; } @@ -37,7 +35,6 @@ UPLOAD_PAGE = """ border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); padding: 20px; - text-align: center; width: 100%; max-width: 500px; } @@ -48,6 +45,13 @@ UPLOAD_PAGE = """ color: #333; } + h2 { + font-size: 20px; + margin-bottom: 20px; + color: #333; + clear: both; + } + form { display: flex; flex-direction: column; @@ -87,37 +91,104 @@ UPLOAD_PAGE = """ font-size: 14px; color: #777; } + + .thumbnail { + width: 100px; + height: 100px; + object-fit: cover; + margin: 10px; + float:left; + }
-

Upload Your File

+

Upload file

+
+ [message] +
-
+
+ Click here to see uploaded files (non-image) at the bottom of this page.
+

Uploaded images:

+
+ [images_html] +
+

Uploaded files:

+
+ [files_html] +
+
""" +def format_size(size): + if size < 1024: + return f"{size} B" + elif size < 1024 * 1024: + return f"{size / 1024:.2f} KB" + elif size < 1024 * 1024 * 1024: + return f"{size / 1024*1024:.2f} MB" + elif size < 1024 * 1024 * 1024: + return f"{size / 1024 * 1024 * 1024:.2f} GB" + else: + return f"{size / 1024 * 1024 * 1024 * 1024:.2f} TB" -async def handle_upload(request): +def get_images(path): + images = [] + for image in pathlib.Path(path).iterdir(): + if image.is_file() and image.suffix in [".png", ".jpg", ".gif", ".jpeg", ".bmp"]: + images.append(image) + return images + +def get_files(path): + images = get_images(path) + files = [] + for file in pathlib.Path(path).iterdir(): + if file.is_file() and file not in images: + files.append(file) + return files + +def create_images_html(url, image_paths): + images_html = "" + for image_path in image_paths: + path = url.rstrip("/") + "/" + image_path.name + images_html += f'{image_path.name}' + return images_html + +def create_files_html(url, file_paths): + files_html = "" + for file_path in file_paths: + path = url.rstrip("/") + "/" + file_path.name + files_html += f'{file_path.name} ({format_size(file_path.stat().st_size)})
' + return files_html + +async def handle_upload(request:web.Request): reader = await request.multipart() field = await reader.next() - + app = request.app if field.name == "file": filename = field.filename - print(filename) - if ".." or "/" in filename: + print(f"Attempting to upload {filename}.") + if "/" in filename: + print(f"Invalid filename: {filename}.") return web.Response(status=400, text="Invalid filename.") - filepath = pathlib.Path(UPLOAD_FOLDER).joinpath(filename) + filepath = pathlib.Path(app.upload_path).joinpath(filename) if filepath.exists(): - return web.Response(status=400, text="File already exists.") + print(f"File {filename} already exists.") + return web.HTTPFound("/?message=File%20already%20exists.") + + pathlib.Path(app.upload_path).mkdir(parents=True, exist_ok=True) with filepath.open("wb") as f: file_size = 0 @@ -126,26 +197,47 @@ async def handle_upload(request): if not chunk: break file_size += len(chunk) - if file_size > max_file_size: + if file_size > app.max_file_size: + print(f"File is too large: {file_size} bytes.") + print(f"Maximum file size is {app.max_file_size} bytes.") + print(f"Deleting file {filename}.") f.close() f.unlink() + print(f"File {filename} deleted.") return web.Response( status=413, text="File is too large. Maximum file size is {} bytes.".format( - max_file_size + app.max_file_size ), ) f.write(chunk) - - return web.Response(text=f"File {filename} uploaded successfully!") + print(f"File {filename} uploaded successfully.") + uploaded_url = app.upload_url.rstrip("/") + "/" + filename + print(f"File {filename} is now available at: {uploaded_url}.") + return web.HTTPFound("/?message=File is succesfully uploaded and is available here:" + uploaded_url) + print("No file uploaded.") return web.Response(status=400, text="No file uploaded.") -async def handle_index(request): - return web.Response(text=UPLOAD_PAGE, content_type="text/html") +async def handle_index(request:web.Request): + image_paths = get_images(request.app.upload_path) + images_html = create_images_html(url=request.app.upload_url,image_paths=image_paths) + file_paths = get_files(request.app.upload_path) + files_html = create_files_html(url=request.app.upload_url,file_paths=file_paths) + html_content = UPLOAD_PAGE.replace("[images_html]",images_html) + html_content = html_content.replace("[files_html]",files_html) + message = request.query.get("message", "") + if request.app.upload_url in message: + url = message[message.find(request.app.upload_url):] + message = message.replace(request.app.upload_url, f" {url}" + if message: + message += "

" + html_content = html_content.replace("[message]",message) + return web.Response(text=html_content, content_type="text/html") -def create_app(upload_path="upload", max_file_size=1024 * 1024 * 50): - app = Rupload(upload_path=upload_path, max_file_size=max_file_size) - app.add_routes([web.get("/", handle_index), web.post("/upload", handle_upload)]) +def create_app(upload_url:str="/", upload_path:str="upload", max_file_size:int=1024 * 1024 * 50): + app = Rupload(upload_url=upload_url, upload_path=upload_path, max_file_size=max_file_size) + app.add_routes([web.get("/", handle_index), web.post("/upload", handle_upload),web.static("/uploads", "uploads")]) return app diff --git a/src/rupload/cli.py b/src/rupload/cli.py index cab2415..eaf1d0f 100644 --- a/src/rupload/cli.py +++ b/src/rupload/cli.py @@ -17,6 +17,12 @@ def parse_args(): default="uploads", help="Directory to store uploaded files.", ) + parser.add_argument( + "--upload_url", + type=str, + default="/uploads/", + help="HTTP(S) URL where the server will serve the uploaded files.", + ) parser.add_argument( "--max_file_size", type=int, @@ -28,7 +34,7 @@ def parse_args(): def main(): args = parse_args() - app = create_app(upload_path=args.upload_folder, max_file_size=args.max_file_size) + app = create_app(upload_url=args.upload_url, upload_path=args.upload_folder, max_file_size=args.max_file_size) web.run_app(app, host=args.hostname, port=args.port)