Last active
December 14, 2023 10:19
-
-
Save MichaelCurrie/19394abc19abd0de4473b595c0e37a3a to your computer and use it in GitHub Desktop.
Drag-and-drop upload files, via JavaScript, to a simple Python 3 HTTP server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Drag-and-drop upload files, via JavaScript, to a simple Python 3 HTTP server |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: "Arial", sands-serif; | |
} | |
.dropzone { | |
width: 300px; | |
height: 300px; | |
border: 2px dashed #ccc; | |
color: #ccc; | |
line-height: 300px; | |
text-align: center; | |
} | |
.dropzone.dragover { | |
border-color: #000; | |
color: #000; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>Drag n' Drop</title> | |
<link rel="stylesheet" type="text/css" href="dropzone.css" /> | |
<script src="dropzone.js"></script> | |
</head> | |
<body> | |
<h1 id="title">Let's try some drag and drop uploading!</h1> | |
<div id="dropzone_element" class="dropzone"> | |
Drop files here to upload | |
</div> | |
<div id="upload_results_element"> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Handle drag and drop into a dropzone_element div: | |
// send the files as a POST request to the server | |
"use strict"; | |
// Only start once the DOM tree is ready | |
if(document.readyState === "complete") { | |
createDropzoneMethods(); | |
} else { | |
document.addEventListener("DOMContentLoaded", createDropzoneMethods); | |
} | |
function createDropzoneMethods() { | |
let dropzone = document.getElementById("dropzone_element"); | |
dropzone.ondragover = function() { | |
this.className = "dropzone dragover"; | |
return false; | |
} | |
dropzone.ondragleave = function() { | |
this.className = "dropzone"; | |
return false; | |
} | |
dropzone.ondrop = function(e) { | |
// Stop browser from simply opening that was just dropped | |
e.preventDefault(); | |
// Restore original dropzone appearance | |
this.className = "dropzone"; | |
upload_files(e.dataTransfer.files) | |
} | |
} | |
function upload_files(files) { | |
let upload_results = document.getElementById("upload_results_element"); | |
let formData = new FormData(), | |
xhr = new XMLHttpRequest(); | |
console.log("Dropped " + String(files.length) + " files."); | |
for(let i=0; i<files.length; i++) { | |
formData.append("file", files[i]); | |
} | |
xhr.onreadystatechange = function() { | |
if(xhr.readyState === XMLHttpRequest.DONE) { | |
alert(xhr.responseText); | |
} | |
console.log(xhr.response); | |
upload_results.innerHTML = this.response; | |
} | |
console.log("Let's upload files: ", formData); | |
xhr.open('POST', 'upload_handler.py', true); // async = true | |
xhr.send(formData); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
import re | |
import sys | |
import os | |
import json | |
from http.server import SimpleHTTPRequestHandler, HTTPServer | |
class FileUploadHTTPRequestHandler(SimpleHTTPRequestHandler): | |
"""An HTTP Server that accepts POST requests and saves them as | |
files in the same folder as this script. | |
""" | |
protocol_version = "HTTP/1.1" | |
def do_POST(self): | |
"""Handle a POST request.""" | |
# Save files received in the POST | |
wasSuccess, files_uploaded = self.handle_file_uploads() | |
# Compose a response to the client | |
response_obj = { | |
"wasSuccess": wasSuccess, | |
"files_uploaded": files_uploaded, | |
"client_address": self.client_address | |
} | |
response_str = json.dumps(response_obj) | |
self.log_message(response_str) | |
# Send our response code, header, and data | |
self.send_response(200) | |
self.send_header("Content-type", "Application/json") | |
self.send_header("Content-Length", len(response_str)) | |
self.end_headers() | |
self.wfile.write(response_str.encode('utf-8')) | |
def read_line(self): | |
line_str = self.rfile.readline().decode('utf-8') | |
self.char_remaining -= len(line_str) | |
return line_str | |
def handle_file_uploads(self): | |
""" | |
Take the post request and save any files received to the same folder | |
as this script. | |
Returns | |
wasSuccess: bool: whether the process was a success | |
files_uploaded: list of string: files that were created | |
""" | |
self.char_remaining = int(self.headers['content-length']) | |
# Find the boundary from content-type, which might look like: | |
# 'multipart/form-data; boundary=----WebKitFormBoundaryUI1LY7c2BiEKGfFk' | |
boundary = self.headers['content-type'].split("=")[1] | |
basepath = self.translate_path(self.path) | |
# Strip this script's name from the path so it's just a folder | |
basepath = os.path.dirname(basepath) | |
# ----WebKitFormBoundaryUI1LY7c2BiEKGfFk | |
line_str = self.read_line() | |
if not boundary in line_str: | |
self.log_message("Content did NOT begin with boundary as " + | |
"it should") | |
return False, [] | |
files_uploaded = [] | |
while self.char_remaining > 0: | |
# Breaking out of this loop on anything except a boundary | |
# an end-of-file will be a failure, so let's assume that | |
wasSuccess = False | |
# Content-Disposition: form-data; name="file"; filename="README.md" | |
line_str = self.read_line() | |
filename = re.findall('Content-Disposition.*name="file"; ' + | |
'filename="(.*)"', line_str) | |
if not filename: | |
self.log_message("Can't find filename " + filename) | |
break | |
else: | |
filename = filename[0] | |
filepath = os.path.join(basepath, filename) | |
try: | |
outfile = open(filepath, 'wb') | |
except IOError: | |
self.log_message("Can't create file " + str(filepath) + | |
" to write; do you have permission to write?") | |
break | |
# Content-Type: application/octet-stream | |
line_str = self.read_line() | |
# Blank line | |
line_str = self.read_line() | |
# First real line of code | |
preline = self.read_line() | |
# Loop through the POST until we find another boundary line, | |
# signifying the end of this file and the possible start of another | |
while self.char_remaining > 0: | |
line_str = self.read_line() | |
# ----WebKitFormBoundaryUI1LY7c2BiEKGfFk | |
if boundary in line_str: | |
preline = preline[0:-1] | |
if preline.endswith('\r'): | |
preline = preline[0:-1] | |
outfile.write(preline.encode('utf-8')) | |
outfile.close() | |
self.log_message("File '%s' upload success!" % filename) | |
files_uploaded.append(filename) | |
# If this was the last file, the session was a success! | |
wasSuccess = True | |
break | |
else: | |
outfile.write(preline.encode('utf-8')) | |
preline = line_str | |
return wasSuccess, files_uploaded | |
if __name__ == "__main__": | |
httpd = HTTPServer(("", 8000), FileUploadHTTPRequestHandler) | |
sa = httpd.socket.getsockname() | |
print("Serving HTTP on", sa[0], "port", sa[1], "...") | |
try: | |
httpd.serve_forever() | |
except KeyboardInterrupt: | |
print("\nKeyboard interrupt received, exiting.") | |
httpd.server_close() | |
sys.exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you mean customize the place on the server the files are saved, you just need to change these lines:
To a hardcoded
basepath
: