simple HTTP server with upload
1 #!/usr/bin/env python 2 3 """Simple HTTP Server With Upload. 4 5 https://github.com/tualatrix/tools/blob/master/SimpleHTTPServerWithUpload.py 6 7 This module builds on BaseHTTPServer by implementing the standard GET 8 and HEAD requests in a fairly straightforward manner. 9 10 """ 11 12 13 import os 14 import posixpath 15 import BaseHTTPServer 16 import urllib 17 import cgi 18 import shutil 19 import mimetypes 20 import re 21 22 __version__ = "0.1" 23 __all__ = ["SimpleHTTPRequestHandler"] 24 __author__ = "bones7456" 25 __home_page__ = "http://li2z.cn/" 26 27 try: 28 from cStringIO import StringIO 29 except ImportError: 30 from StringIO import StringIO 31 32 33 class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 34 35 """Simple HTTP request handler with GET/HEAD/POST commands. 36 37 This serves files from the current directory and any of its 38 subdirectories. The MIME type for files is determined by 39 calling the .guess_type() method. And can reveive file uploaded 40 by client. 41 42 The GET/HEAD/POST requests are identical except that the HEAD 43 request omits the actual contents of the file. 44 45 """ 46 47 server_version = "SimpleHTTPWithUpload/" + __version__ 48 49 def do_GET(self): 50 """Serve a GET request.""" 51 f = self.send_head() 52 if f: 53 self.copyfile(f, self.wfile) 54 f.close() 55 56 def do_HEAD(self): 57 """Serve a HEAD request.""" 58 f = self.send_head() 59 if f: 60 f.close() 61 62 def do_POST(self): 63 """Serve a POST request.""" 64 r, info = self.deal_post_data() 65 print r, info, "by: ", self.client_address 66 f = StringIO() 67 f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') 68 f.write("<html>\n<meta charset='UTF-8'>\n<title>Upload Result Page</title>\n") 69 f.write("<html>\n<title>Upload Result Page</title>\n") 70 f.write("<body>\n<h2>Upload Result Page</h2>\n") 71 f.write("<hr>\n") 72 if r: 73 f.write("<strong>Success:</strong>") 74 else: 75 f.write("<strong>Failed:</strong>") 76 f.write(info) 77 f.write("<br><a href=\"%s\">back</a>" % self.headers['referer']) 78 f.write("<hr><small>Powered By: bones7456, check new version at ") 79 f.write("<a href=\"http://li2z.cn/?s=SimpleHTTPServerWithUpload\">") 80 f.write("here</a>.</small></body>\n</html>\n") 81 length = f.tell() 82 f.seek(0) 83 self.send_response(200) 84 self.send_header("Content-type", "text/html") 85 self.send_header("Content-Length", str(length)) 86 self.end_headers() 87 if f: 88 self.copyfile(f, self.wfile) 89 f.close() 90 91 def deal_post_data(self): 92 boundary = self.headers.plisttext.split("=")[1] 93 remainbytes = int(self.headers['content-length']) 94 line = self.rfile.readline() 95 remainbytes -= len(line) 96 if boundary not in line: 97 return (False, "Content NOT begin with boundary") 98 line = self.rfile.readline() 99 remainbytes -= len(line) 100 fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line) 101 if not fn: 102 return (False, "Can't find out file name...") 103 path = self.translate_path(self.path) 104 fn = os.path.join(path, fn[0]) 105 while os.path.exists(fn): 106 fn += "_" 107 line = self.rfile.readline() 108 remainbytes -= len(line) 109 line = self.rfile.readline() 110 remainbytes -= len(line) 111 try: 112 out = open(fn, 'wb') 113 except IOError: 114 return (False, "Can't create file to write, do you have permission to write?") 115 116 preline = self.rfile.readline() 117 remainbytes -= len(preline) 118 while remainbytes > 0: 119 line = self.rfile.readline() 120 remainbytes -= len(line) 121 if boundary in line: 122 preline = preline[0:-1] 123 if preline.endswith('\r'): 124 preline = preline[0:-1] 125 out.write(preline) 126 out.close() 127 return (True, "File '%s' upload success!" % fn) 128 else: 129 out.write(preline) 130 preline = line 131 return (False, "Unexpect Ends of data.") 132 133 def send_head(self): 134 """Common code for GET and HEAD commands. 135 136 This sends the response code and MIME headers. 137 138 Return value is either a file object (which has to be copied 139 to the outputfile by the caller unless the command was HEAD, 140 and must be closed by the caller under all circumstances), or 141 None, in which case the caller has nothing further to do. 142 143 """ 144 path = self.translate_path(self.path) 145 f = None 146 if os.path.isdir(path): 147 if not self.path.endswith('/'): 148 # redirect browser - doing basically what apache does 149 self.send_response(301) 150 self.send_header("Location", self.path + "/") 151 self.end_headers() 152 return None 153 for index in "index.html", "index.htm": 154 index = os.path.join(path, index) 155 if os.path.exists(index): 156 path = index 157 break 158 else: 159 return self.list_directory(path) 160 ctype = self.guess_type(path) 161 try: 162 # Always read in binary mode. Opening files in text mode may cause 163 # newline translations, making the actual size of the content 164 # transmitted *less* than the content-length! 165 f = open(path, 'rb') 166 except IOError: 167 self.send_error(404, "File not found") 168 return None 169 self.send_response(200) 170 self.send_header("Content-type", ctype) 171 fs = os.fstat(f.fileno()) 172 self.send_header("Content-Length", str(fs[6])) 173 self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 174 self.end_headers() 175 return f 176 177 def list_directory(self, path): 178 """Helper to produce a directory listing (absent index.html). 179 180 Return value is either a file object, or None (indicating an 181 error). In either case, the headers are sent, making the 182 interface the same as for send_head(). 183 184 """ 185 try: 186 list = os.listdir(path) 187 except os.error: 188 self.send_error(404, "No permission to list directory") 189 return None 190 list.sort(key=lambda a: a.lower()) 191 f = StringIO() 192 displaypath = cgi.escape(urllib.unquote(self.path)) 193 f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') 194 f.write("<html><meta charset='UTF-8'>\n<title>Directory listing for %s</title>\n" % displaypath) 195 f.write("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath) 196 f.write("<hr>\n") 197 f.write("<form ENCTYPE=\"multipart/form-data\" method=\"post\">") 198 f.write("<input name=\"file\" type=\"file\"/>") 199 f.write("<input type=\"submit\" value=\"upload\"/></form>\n") 200 f.write("<hr>\n<ul>\n") 201 for name in list: 202 if '.py' not in name and '.html' not in name: 203 fullname = os.path.join(path, name) 204 displayname = linkname = name 205 # Append / for directories or @ for symbolic links 206 if os.path.isdir(fullname): 207 displayname = name + "/" 208 linkname = name + "/" 209 if os.path.islink(fullname): 210 displayname = name + "@" 211 # Note: a link to a directory displays with @ and links with / 212 f.write('<li><a href="%s">%s</a>\n' 213 % (urllib.quote(linkname), cgi.escape(displayname))) 214 f.write("</ul>\n<hr>\n</body>\n</html>\n") 215 length = f.tell() 216 f.seek(0) 217 self.send_response(200) 218 self.send_header("Content-type", "text/html") 219 self.send_header("Content-Length", str(length)) 220 self.end_headers() 221 return f 222 223 def translate_path(self, path): 224 """Translate a /-separated PATH to the local filename syntax. 225 226 Components that mean special things to the local file system 227 (e.g. drive or directory names) are ignored. (XXX They should 228 probably be diagnosed.) 229 230 """ 231 # abandon query parameters 232 path = path.split('?', 1)[0] 233 path = path.split('#', 1)[0] 234 path = posixpath.normpath(urllib.unquote(path)) 235 words = path.split('/') 236 words = filter(None, words) 237 path = os.getcwd() 238 for word in words: 239 drive, word = os.path.splitdrive(word) 240 head, word = os.path.split(word) 241 if word in (os.curdir, os.pardir): 242 continue 243 path = os.path.join(path, word) 244 return path 245 246 def copyfile(self, source, outputfile): 247 """Copy all data between two file objects. 248 249 The SOURCE argument is a file object open for reading 250 (or anything with a read() method) and the DESTINATION 251 argument is a file object open for writing (or 252 anything with a write() method). 253 254 The only reason for overriding this would be to change 255 the block size or perhaps to replace newlines by CRLF 256 -- note however that this the default server uses this 257 to copy binary data as well. 258 259 """ 260 shutil.copyfileobj(source, outputfile) 261 262 def guess_type(self, path): 263 """Guess the type of a file. 264 265 Argument is a PATH (a filename). 266 267 Return value is a string of the form type/subtype, 268 usable for a MIME Content-type header. 269 270 The default implementation looks the file's extension 271 up in the table self.extensions_map, using application/octet-stream 272 as a default; however it would be permissible (if 273 slow) to look inside the data to make a better guess. 274 275 """ 276 277 base, ext = posixpath.splitext(path) 278 if ext in self.extensions_map: 279 return self.extensions_map[ext] 280 ext = ext.lower() 281 if ext in self.extensions_map: 282 return self.extensions_map[ext] 283 else: 284 return self.extensions_map[''] 285 286 if not mimetypes.inited: 287 mimetypes.init() # try to read system mime.types 288 extensions_map = mimetypes.types_map.copy() 289 extensions_map.update({ 290 '': 'application/octet-stream', # Default 291 '.py': 'text/plain', 292 '.c': 'text/plain', 293 '.h': 'text/plain', 294 }) 295 296 297 def test(HandlerClass=SimpleHTTPRequestHandler, 298 ServerClass=BaseHTTPServer.HTTPServer): 299 BaseHTTPServer.test(HandlerClass, ServerClass) 300 301 if __name__ == '__main__': 302 test()