Updated readme.
This commit is contained in:
parent
3e72ecab20
commit
67d91a42e5
5
Makefile
5
Makefile
@ -1,4 +1,7 @@
|
|||||||
all: ensure_env build serve
|
all: ensure_env clean build serve
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-@rm -rf src/rupload/__pycache__
|
||||||
|
|
||||||
ensure_env:
|
ensure_env:
|
||||||
-@python3 -m venv .venv
|
-@python3 -m venv .venv
|
||||||
|
30
README.md
30
README.md
@ -12,6 +12,30 @@ python3 -m venv .venv
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
```
|
`rupload.serve` [-h]
|
||||||
rupload.serve [host(127.0.0.1)] [port] [destination path for uploads] [max file size in bytes]
|
[--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).
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -5,15 +5,13 @@ import pathlib
|
|||||||
|
|
||||||
|
|
||||||
class Rupload(web.Application):
|
class Rupload(web.Application):
|
||||||
def __init__(self, upload_path, max_file_size):
|
def __init__(self, upload_url:str="/uploads/", upload_path:str="uploads", max_file_size:int=1024*1024*50):
|
||||||
self.upload_path = upload_path
|
self.upload_path = upload_path
|
||||||
self.max_file_size = max_file_size
|
self.max_file_size = max_file_size
|
||||||
|
self.upload_url = upload_url
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
UPLOAD_FOLDER = "uploads"
|
|
||||||
pathlib.Path(UPLOAD_FOLDER).mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
UPLOAD_PAGE = """
|
UPLOAD_PAGE = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -28,7 +26,7 @@ UPLOAD_PAGE = """
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +35,6 @@ UPLOAD_PAGE = """
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
@ -48,6 +45,13 @@ UPLOAD_PAGE = """
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -87,37 +91,104 @@ UPLOAD_PAGE = """
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin: 10px;
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Upload Your File</h1>
|
<h1>Upload file</h1>
|
||||||
|
<div class="message">
|
||||||
|
[message]
|
||||||
|
</div>
|
||||||
<form action="/upload" method="POST" enctype="multipart/form-data">
|
<form action="/upload" method="POST" enctype="multipart/form-data">
|
||||||
<label for="file">Choose a file to upload:</label>
|
|
||||||
<input type="file" name="file" id="file" required>
|
<input type="file" name="file" id="file" required>
|
||||||
<button type="submit">Upload</button>
|
<button type="submit">Upload</button>
|
||||||
</form>
|
</form>
|
||||||
|
<br />
|
||||||
|
Click <a href="#files">here</a> to see <a href="#files">uploaded files</a> (non-image) at the bottom of this page.<br />
|
||||||
|
<h2>Uploaded images:</h2>
|
||||||
|
<div class="container-image">
|
||||||
|
[images_html]
|
||||||
|
</div>
|
||||||
|
<h2 id="files">Uploaded files:</h2>
|
||||||
|
<div class="container-file">
|
||||||
|
[files_html]
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
© 2024 Retoor
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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'<img class="thumbnail" src="{path}" alt="{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'<a href="{path}">{file_path.name}</a> ({format_size(file_path.stat().st_size)})<br>'
|
||||||
|
return files_html
|
||||||
|
|
||||||
|
async def handle_upload(request:web.Request):
|
||||||
reader = await request.multipart()
|
reader = await request.multipart()
|
||||||
field = await reader.next()
|
field = await reader.next()
|
||||||
|
app = request.app
|
||||||
if field.name == "file":
|
if field.name == "file":
|
||||||
|
|
||||||
filename = field.filename
|
filename = field.filename
|
||||||
print(filename)
|
print(f"Attempting to upload {filename}.")
|
||||||
if ".." or "/" in filename:
|
if "/" in filename:
|
||||||
|
print(f"Invalid filename: {filename}.")
|
||||||
return web.Response(status=400, text="Invalid 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():
|
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:
|
with filepath.open("wb") as f:
|
||||||
file_size = 0
|
file_size = 0
|
||||||
@ -126,26 +197,47 @@ async def handle_upload(request):
|
|||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
file_size += len(chunk)
|
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.close()
|
||||||
f.unlink()
|
f.unlink()
|
||||||
|
print(f"File {filename} deleted.")
|
||||||
return web.Response(
|
return web.Response(
|
||||||
status=413,
|
status=413,
|
||||||
text="File is too large. Maximum file size is {} bytes.".format(
|
text="File is too large. Maximum file size is {} bytes.".format(
|
||||||
max_file_size
|
app.max_file_size
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
print(f"File {filename} uploaded successfully.")
|
||||||
return web.Response(text=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.")
|
return web.Response(status=400, text="No file uploaded.")
|
||||||
|
|
||||||
|
|
||||||
async def handle_index(request):
|
async def handle_index(request:web.Request):
|
||||||
return web.Response(text=UPLOAD_PAGE, content_type="text/html")
|
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" <a href=\"{url}")
|
||||||
|
message += f"\">{url}</a>"
|
||||||
|
if message:
|
||||||
|
message += "<br /><br />"
|
||||||
|
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):
|
def create_app(upload_url:str="/", upload_path:str="upload", max_file_size:int=1024 * 1024 * 50):
|
||||||
app = Rupload(upload_path=upload_path, max_file_size=max_file_size)
|
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)])
|
app.add_routes([web.get("/", handle_index), web.post("/upload", handle_upload),web.static("/uploads", "uploads")])
|
||||||
return app
|
return app
|
||||||
|
@ -17,6 +17,12 @@ def parse_args():
|
|||||||
default="uploads",
|
default="uploads",
|
||||||
help="Directory to store uploaded files.",
|
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(
|
parser.add_argument(
|
||||||
"--max_file_size",
|
"--max_file_size",
|
||||||
type=int,
|
type=int,
|
||||||
@ -28,7 +34,7 @@ def parse_args():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
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)
|
web.run_app(app, host=args.hostname, port=args.port)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user