Basic認証付きデータストアをGoogle App Engine上に

Basic認証を用いて,Google App Engine上に任意のデータをアップロード,ダウンロードできる仕掛けを作ってみた.このご時世,自前サーバを外にさらすのはいやなので,Googleさまにお任せしようと.

パスをデータの名前として扱い,ダウンロード時のContent-Typeはパスの拡張子で適当に決めている.本当はアップロード時にContent-Typeを設定してもらって,それを使うようにした方がいいのかもしれないが.データはBlobとして扱う.アップロード, ダウンロード,消去は同じURLに対して,PUT/GET/DELETEメソッドで行う.こういうのREST風っていうんでいいんですよね?

ユーザごとに別の名前空間としているので,ユーザAのhttp://example.com/dataと ユーザBのhttp://example.com/dataは別のデータとなる.

# -*- coding: utf-8 -*-
import wsgiref.handlers

from google.appengine.ext import webapp, db
from google.appengine.api import users
from google.appengine.ext.webapp import template
from basicAuth import basicAuth
import os
import logging

typeDict = {"txt":  "text/plain",
	    "htm":  "text/html",
	    "html": "text/html",
	    "xml":  "text/xml",
	    "gif":  "image/gif",
	    "jpg":  "image/jpeg",
	    "jpeg": "image/jpeg",
	    "png":  "image/png",
	    "doc":  "application/msword",
	    "pdf":  "application/pdf"}
	    
def lookupType(path):
  try:
    ext = path.split(".")[-1].lower()
    return typeDict[ext]
  except KeyError:
    return "application/octet-stream"

class Store(db.Model):
  """データモデルの定義"""
  path = db.StringProperty()
  user = db.StringProperty()
  data = db.BlobProperty()

userDict = {"userA": "passA",
	    "userB": "passB"}

class MainPage(webapp.RequestHandler):
  def _fetch(self, path, user):
    """storeを取得する"""
    q = db.GqlQuery("SELECT * FROM Store WHERE path = :1 AND user = :2", 
		    path, user)
    return q.get()

####### GET METHOD
  @basicAuth(userDict, "simplestore")
  def get(self):
    logging.info('authenticated as ' + self.request.basic_user)
    store = self._fetch(self.request.path, self.request.basic_user)
    if store:    # storeがあれば中身を出力
      self.response.out.write(store.data)
      self.response.headers["Content-Type"] = \
	  lookupType(self.request.path)
    else:        # なければエラー
      self.response.set_status(404)
      self.response.out.write('<body><h1>Not Found</h1></body>\n')

####### PUT METHOD
  @basicAuth(userDict, "simplestore")		
  def put(self):
    store = self._fetch(self.request.path, self.request.basic_user)
    if store:    # 既存のstoreがあればそれをアップデート
      logging.info("update %s for user %s" % 
		   (self.request.path, self.request.basic_user))
      store.data = self.request.body
      store.put()
    else:        # なければ新しく作成
      store = Store(path  = self.request.path,
		    user = self.request.basic_user,
		    data = self.request.body)
      store.put()
    self.response.set_status(200)

####### DELETE METHOD
  @basicAuth(userDict, "simplestore")		
  def delete(self):
    store = self._fetch(self.request.path, self.request.basic_user)
    if store:    # 既存のstoreがあればそれを削除
      logging.info("deleting %s for user %s" % 
		   (self.request.path, self.request.basic_user))
      store.delete()
      self.response.set_status(200)
    else:
      logging.info("failed to delete %s for user %s" % 
		   (self.request.path, self.request.basic_user))
      self.response.set_status(404)
      self.response.out.write('<body><h1>Not Found</h1></body>\n')
 
def main():
  application = webapp.WSGIApplication([('/.*', MainPage)], debug = True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
  main()

使い方

  • アップロードにはcurlを使う.
> curl -u userA:passA -T DATA http://example.com/some.path.ext
  • ダウンロードはブラウザとかでOK.
  • 削除もcurlを使うと簡単
> curl -u userA:passA -X DELETE http://example.com/some.path.ext