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()