17

I'm trying to make a simple python webserver to save text that is Posted to a file called store.json which is in the same folder as the python script. Here is half of my code, can someone tell me what the rest should be?

import string,cgi,time
from os import curdir, sep
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
#import pri

class StoreHandler(BaseHTTPRequestHandler):
def do_GET(self):
    try:
        if self.path == "/store.json":
            f = open(curdir + sep + "store.json") #self.path has /test.html
            self.send_response(200)
            self.send_header('Content-type','text/json')
            self.end_headers()
            self.wfile.write(f.read())
            f.close()
            return
        return
    except IOError:
        self.send_error(404,'File Not Found: %s' % self.path)
def do_POST(self):
    //if the url is 'store.json' then
    //what do I do here?

def main():
    try:
        server = HTTPServer(('', 80), StoreHandler)
        print 'started...'
        server.serve_forever()
    except KeyboardInterrupt:
        print '^C received, shutting down server'
        server.socket.close()
if __name__ == '__main__':
    main()
Arlen Beiler
  • 15,336
  • 34
  • 92
  • 135

2 Answers2

35

Here's the general idea:

from os import curdir
from os.path import join as pjoin

from http.server import BaseHTTPRequestHandler, HTTPServer

class StoreHandler(BaseHTTPRequestHandler):
    store_path = pjoin(curdir, 'store.json')

    def do_GET(self):
        if self.path == '/store.json':
            with open(self.store_path) as fh:
                self.send_response(200)
                self.send_header('Content-type', 'text/json')
                self.end_headers()
                self.wfile.write(fh.read().encode())

    def do_POST(self):
        if self.path == '/store.json':
            length = self.headers['content-length']
            data = self.rfile.read(int(length))

            with open(self.store_path, 'w') as fh:
                fh.write(data.decode())

            self.send_response(200)


server = HTTPServer(('', 8080), StoreHandler)
server.serve_forever()
$ curl -X POST --data "one two three four" localhost:8080/store.json
$ curl -X GET localhost:8080/store.json    
one two three four%
gvalkov
  • 3,977
  • 30
  • 31
  • It says `ImportError: No module named 'BaseHTTPServer'` – Arlen Beiler Oct 30 '12 at 19:57
  • 2
    Ok, you need to change `from BaseHTTPServer` to `from http.server` – Arlen Beiler Oct 30 '12 at 20:09
  • 1
    Sorry, didn't notice the `-3.x` part of the tag. – gvalkov Oct 30 '12 at 22:22
  • 1
    One other thing I discovered is that you have to add `'rb'` as an argument to the open command, otherwise you get a type exception. `With open(self.store_path,'rb') as fh:` – Arlen Beiler Oct 30 '12 at 23:27
  • I'm getting an exception when I try to post. `AttributeError: 'HTTPMessage' object has no attribute 'getheader'` – Arlen Beiler Oct 30 '12 at 23:34
  • 2
    Ok, I changed the offending line to `self.headers['content-length']`. I also had to change the `post` `open` to `'wb'` – Arlen Beiler Oct 30 '12 at 23:40
  • I think I nailed the last Python 3 issue (bytes/str) in the last edit as well. Cheers. – gvalkov Oct 30 '12 at 23:52
  • To get it to work, I had to change *self.path* to *self.store_path* in the if stateent. Like this: `if self.store_path == './store.json':` – Stan James Oct 18 '16 at 17:02
  • Will it also work if the uploaded file is binary? Should I open the file in such case with `'wb'` instead of `'w'`? – SomethingSomething Dec 08 '16 at 12:11
  • 1
    @SomethingSomething yes exactly. Remove the `content type` line, remove `decode()` and `encode()` and use `wb` and `rb` instead of `w` and (nothing). – phil294 Jul 22 '17 at 22:13
  • Seemingly, self.end_headers() needs to be added after "self.send_response(200)" in do_POST. Otherwise, POST request is treated as timed out. I tested with multiple HTTP clients. – Aki24x Mar 09 '18 at 00:47
  • As API can be called parallel so is it safe to write file this way? – Mayur Nov 14 '19 at 07:21
  • With `open(self.store_path)`, you're creating a text stream, which decodes the bytes into a string. Then with `fh.read().encode()`, you're encoding the string into bytes again. It would make more sense to use the bytes directly with `open(self.store_path, 'rb')`, then the call to `encode()` is no longer necessary. – Big McLargeHuge Feb 09 '20 at 21:33
  • @phil294 I'm confused by your comment. Are you suggesting to remove the `content-type` header from the `do_GET` method? – Big McLargeHuge Feb 09 '20 at 21:37
  • @DavidKennedy I am really sorry, I have no idea either with what I meant by that – phil294 Feb 10 '20 at 13:12
11

Important thing is that you will have to build cgi.FieldStorage properly from the raw posted data e.g.

form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={'REQUEST_METHOD':'POST',
                     'CONTENT_TYPE':self.headers['Content-Type'],
                     })

after that it is easy to dump file, here is a simple handler which shows a form on do_GET to upload any file user chooses and saves that file to /tmp in do_POST when form is POSTed

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler

import cgi

class StoreHandler(BaseHTTPRequestHandler):
    def do_POST(self):

        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={'REQUEST_METHOD':'POST',
                     'CONTENT_TYPE':self.headers['Content-Type'],
                     })
        filename = form['file'].filename
        data = form['file'].file.read()
        open("/tmp/%s"%filename, "wb").write(data)

        self.respond("uploaded %s, thanks"%filename)

    def do_GET(self):
        response = """
        <html><body>
        <form enctype="multipart/form-data" method="post">
        <p>File: <input type="file" name="file"></p>
        <p><input type="submit" value="Upload"></p>
        </form>
        </body></html>
        """        

        self.respond(response)

    def respond(self, response, status=200):
        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-length", len(response))
        self.end_headers()
        self.wfile.write(response)  

Also note that self.respond is not a standard method I just added it for quickly returning some response.

Anurag Uniyal
  • 85,954
  • 40
  • 175
  • 219