This commit is contained in:
Darko Luketic 2015-03-21 16:02:38 +01:00
commit 831310875d
3 changed files with 413 additions and 0 deletions

81
README.md Normal file
View File

@ -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

291
file.go Normal file
View File

@ -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) {
}

41
helpers.go Normal file
View File

@ -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
}