initial
This commit is contained in:
commit
831310875d
81
README.md
Normal file
81
README.md
Normal 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
291
file.go
Normal 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
41
helpers.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user