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