From 831310875d89e71f78cb61578adcea997defcad8 Mon Sep 17 00:00:00 2001 From: Darko Luketic Date: Sat, 21 Mar 2015 16:02:38 +0100 Subject: [PATCH] initial --- README.md | 81 +++++++++++++++ file.go | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 41 ++++++++ 3 files changed, 413 insertions(+) create mode 100644 README.md create mode 100644 file.go create mode 100644 helpers.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d8951f --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# file + +Upload, Download, Delete and list files (optional) as JSON + +## How + +```go +package main + +import ( + "github.com/gomango/auth" + "github.com/gomango/mblog/blog" + "gopkg.in/mgo.v2" + "log" + "net/http" +) + +func main() { + ms, e := mgo.Dial("localhost") + if e != nil { + log.Fatalln(e.Error()) + } + authopts := auth.Options{ + Host: "file.dev.luketic", + MailFrom: "root@localhost", + MailSupport: "postmaster@localhost", + TemplatePath: "/home/darko/go/src/github.com/gomango/authtemplates", + MailTemplatePath: "/home/darko/go/src/github.com/gomango/authemailtemplates/pongo2", + XSRFkey: auth.GenKey(128), + Database: "testmblog", + Account: "account", + Resetcode: "resetcode", + Profile: "profile", + AESkey: auth.GenKey(32), + HMACkey: auth.GenKey(512), + BcryptPasswordCost: 12, + } + ah := auth.NewAuthHandler(ms, authopts) + http.Handle("/account/", http.StripPrefix("/account/", ah)) + fileopts := blog.FileHandlerOptions{ + Prefix: "images", + DB: "testfile", + MS: ms, + AllowDuplicate: false, + DisplayIndex: true, + } + fh := blog.NewFileHandler(fileopts) + http.Handle("/images/", http.StripPrefix("/images/", fh)) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +``` + +## Interface + +PUT and DELETE methods require either + +* a cookie with the JWT token acquired by logging in ("token=JWT") +* a HTTP Header Authorization: Bearer JWT + +Where JWT is the JWT string + +### AllowDuplicate: true + +GET / +PUT / +DELETE /{bsonId:[a-fA-F0-9]{24}} + +### AllowDuplicate: false + +GET / +PUT / +DELETE /{filename:.*} + +### Other + +For auth methods see https://github.com/gomango/auth + +## TODO + +* Thumbnails diff --git a/file.go b/file.go new file mode 100644 index 0000000..e6dda15 --- /dev/null +++ b/file.go @@ -0,0 +1,291 @@ +package blog + +import ( + "bytes" + "github.com/gomango/auth" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + "io" + "net/http" + "regexp" + "strconv" + "time" +) + +var bsonRegex = regexp.MustCompile(`[a-fA-F0-9]{24}`) + +type Metadata struct { + AccountId string `bson:"account_id,omitempty"` + ProfileId string `bson:"profile_id,omitempty"` +} + +type FileHandler struct { + options FileHandlerOptions +} + +type FileHandlerOptions struct { + Prefix string // Prefix is more or less the collection name + DB string // Database name + MS *mgo.Session // The main mgo Session + AllowDuplicate bool // Wether to allow more than 1 same named file + DisplayIndex bool // wether to display a directory index consisting of all files +} + +func NewFileHandler(opts FileHandlerOptions) *FileHandler { + return &FileHandler{options: opts} +} + +func (h *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + switch r.URL.Path { + case "": + if !h.options.DisplayIndex { + http.NotFound(w, r) + return + } + ms := h.options.MS.Copy() + gfs := ms.DB(h.options.DB).GridFS(h.options.Prefix) + type File struct { + Id string `json:"id"` + Name string `json:"name"` + Size int64 `json:"size,string"` + ContentType string `json:"contenttype"` + UploadDate time.Time `json:"uploaddate"` + } + it := gfs.Find(nil).Iter() + var f *mgo.GridFile + files := []*File{} + for gfs.OpenNext(it, &f) { + id, _ := f.Id().(bson.ObjectId) + files = append(files, &File{Id: id.Hex(), Name: f.Name(), Size: f.Size(), ContentType: f.ContentType(), UploadDate: f.UploadDate()}) + } + if it.Close() != nil { + panic(it.Close()) + } + if e := writeJSON(w, files); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + } + default: + ms := h.options.MS.Copy() + gfs := ms.DB(h.options.DB).GridFS(h.options.Prefix) + var file *mgo.GridFile + var e error + if h.options.AllowDuplicate { + if !bsonRegex.MatchString(r.URL.Path) { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + // find by id + file, e = gfs.OpenId(bson.ObjectIdHex(r.URL.Path)) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + } else { + // find by filename + file, e = gfs.Open(r.URL.Path) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + } + w.Header().Set("Content-Type", file.ContentType()) + w.Header().Set("Content-Length", strconv.FormatInt(file.Size(), 10)) + if n, e := io.Copy(w, file); e != nil { + panic(e) + } else if n != file.Size() { + panic("file: size and number written not the same") + } + if e := file.Close(); e != nil { + panic(e) + } + + } + case "PUT": + // check token + + ts := tokenFromRequest(r) + if ts == "" { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + token, e := auth.VerifyToken(ts) + if e != nil { + http.Error(w, e.Error(), http.StatusForbidden) + return + } + + // upload and insert + ms := h.options.MS.Copy() + gfs := ms.DB(h.options.DB).GridFS(h.options.Prefix) + if !h.options.AllowDuplicate { + // No duplicate files + //c := ms.DB(h.options.DB).C(h.options.Prefix + ".files") + c := gfs.Files + query := c.Find(bson.M{"filename": r.URL.Path}) + count, e := query.Count() + + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + if count > 0 { + type File struct { + Id bson.ObjectId `bson:_id"` + ChunkSize int64 `bson:"chunkSize"` + UploadDate time.Time `bson:"uploadDate"` + Length int64 `bson:"length"` + MD5 string `bson:"md5"` + Filename string `bson:"filename"` + } + f := new(File) + if e := query.One(f); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + if e := gfs.Remove(f.Filename); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + } + } + + f, e := gfs.Create(r.URL.Path) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + b := bytes.NewBuffer(nil) + n, e := io.Copy(b, r.Body) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + meta := new(Metadata) + meta.AccountId = token.Claims.AccountId + meta.ProfileId = token.Claims.ProfileId + + f.SetMeta(meta) + f.SetContentType(http.DetectContentType(b.Bytes())) + + n, e = io.Copy(f, b) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + if e := f.Close(); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + id, _ := f.Id().(bson.ObjectId) + + out := struct { + Id string `json:"id"` + Written int64 `json:"written,string"` + }{ + Id: id.Hex(), + Written: n, + } + if e := writeJSON(w, out); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + } + case "DELETE": + // check token + + ts := tokenFromRequest(r) + if ts == "" { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + token, e := auth.VerifyToken(ts) + if e != nil { + http.Error(w, e.Error(), http.StatusForbidden) + return + } + + ms := h.options.MS.Copy() + gfs := ms.DB(h.options.DB).GridFS(h.options.Prefix) + + isadmin := false + for _, role := range token.Claims.Roles { + if role == "admin" { + isadmin = true + break + } + } + + if h.options.AllowDuplicate { + if !bsonRegex.MatchString(r.URL.Path) { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + file, e := gfs.OpenId(bson.ObjectIdHex(r.URL.Path)) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + + meta := new(Metadata) + + if e := file.GetMeta(meta); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + if e := file.Close(); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + + if token.Claims.AccountId != meta.AccountId { + if !isadmin { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + } + return + } + // remove file by id + if e := gfs.RemoveId(bson.ObjectIdHex(r.URL.Path)); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(200) + } else { + file, e := gfs.Open(r.URL.Path) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + meta := new(Metadata) + + if e := file.GetMeta(meta); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + if e := file.Close(); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + if token.Claims.AccountId != meta.AccountId { + if !isadmin { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + } + return + } + + // remove file by name + if e := gfs.Remove(r.URL.Path); e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + } + w.WriteHeader(200) + } + + default: + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + } +} + +func AddFile(w http.ResponseWriter, r *http.Request) { + +} diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..7e00cfd --- /dev/null +++ b/helpers.go @@ -0,0 +1,41 @@ +package blog + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" +) + +func readJSON(r *http.Request, data interface{}) error { + decoder := json.NewDecoder(r.Body) + return decoder.Decode(data) +} + +func writeJSON(w http.ResponseWriter, data interface{}) error { + if d, err := json.Marshal(data); err != nil { + return err + } else { + w.Header().Set("Content-Length", strconv.Itoa(len(d))) + w.Header().Set("Content-Type", "application/json") + w.Write(d) + } + return nil +} + +func tokenFromRequest(r *http.Request) string { + var token string + tokencookie, e := r.Cookie("token") + if e == nil { + token = tokencookie.Value + } + + if token == "" { + tmp := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + if strings.ToLower(tmp[0]) != "bearer" { + return "" + } + token = tmp[1] + } + return token +}