From 1978883cd7b75ede2f9105b9375ff705dec58e1e Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 10 Jun 2018 20:11:32 +0200 Subject: [PATCH] Added File model (still missing edit/remove), fixed uploading to space always failing, added /upload endpoint to rpdata-grapihql server --- cmd/rpdata-graphiql/main.go | 39 ++++++++++ internal/store/space.go | 9 ++- model/file/file.go | 138 ++++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 model/file/file.go diff --git a/cmd/rpdata-graphiql/main.go b/cmd/rpdata-graphiql/main.go index 45fec3c..0442652 100644 --- a/cmd/rpdata-graphiql/main.go +++ b/cmd/rpdata-graphiql/main.go @@ -1,12 +1,14 @@ package main import ( + "encoding/json" "log" "net/http" "git.aiterp.net/rpdata/api/internal/session" "git.aiterp.net/rpdata/api/internal/store" "git.aiterp.net/rpdata/api/loader" + "git.aiterp.net/rpdata/api/model/file" logModel "git.aiterp.net/rpdata/api/model/log" "git.aiterp.net/rpdata/api/resolver" "git.aiterp.net/rpdata/api/schema" @@ -50,6 +52,43 @@ func main() { relayHandler.ServeHTTP(w, r) }) + http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) { + r = session.Load(w, r) + sess := session.FromContext(r.Context()) + user := sess.User() + if user == nil || !user.Permitted("file.upload") { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if err := r.ParseMultipartForm(16384); err != nil { + http.Error(w, "Internal (1): "+err.Error(), http.StatusInternalServerError) + return + } + + formFile, header, err := r.FormFile("file") + if err != nil || header == nil || formFile == nil { + http.Error(w, "No file provided", http.StatusBadRequest) + return + } + + file, err := file.Upload(r.Context(), header.Filename, header.Header.Get("Content-Type"), user.ID, header.Size, formFile) + if err != nil { + http.Error(w, "Internal (2): "+err.Error(), http.StatusInternalServerError) + return + } + + json, err := json.Marshal(file) + if err != nil { + http.Error(w, "Internal (3): "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(json) + }) + log.Fatal(http.ListenAndServe(":17000", nil)) } diff --git a/internal/store/space.go b/internal/store/space.go index b515272..d057bb4 100644 --- a/internal/store/space.go +++ b/internal/store/space.go @@ -59,7 +59,7 @@ func UploadFile(ctx context.Context, folder string, name string, mimeType string return "", err } - _, err = spaceClient.StatObject(spaceBucket, path, minio.StatObjectOptions{}) + _, err = spaceClient.StatObject(spaceBucket, spaceRoot+"/"+path, minio.StatObjectOptions{}) if err != nil { return "", err } @@ -67,6 +67,13 @@ func UploadFile(ctx context.Context, folder string, name string, mimeType string return path, nil } +// RemoveFile removes a file from the space +func RemoveFile(folder string, name string) error { + path := folder + "/" + name + + return spaceClient.RemoveObject(spaceBucket, spaceRoot+"/"+path) +} + // DownloadFile opens a file for download, using the same path format as the UploadFile function. Remember to Close it! func DownloadFile(ctx context.Context, path string) (io.ReadCloser, error) { return spaceClient.GetObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, minio.GetObjectOptions{}) diff --git a/model/file/file.go b/model/file/file.go new file mode 100644 index 0000000..6dded9a --- /dev/null +++ b/model/file/file.go @@ -0,0 +1,138 @@ +package file + +import ( + "context" + "crypto/rand" + "encoding/binary" + "io" + "strconv" + "time" + + "git.aiterp.net/rpdata/api/internal/store" + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" +) + +var fileCollection *mgo.Collection + +// A File is a record of a file stored in the Space. +type File struct { + ID string `bson:"_id" json:"id"` + Time time.Time `bson:"time" json:"time"` + Public bool `bson:"public" json:"public"` + Name string `bson:"name" json:"name"` + MimeType string `bson:"mimeType" json:"mimeType"` + Size int64 `bson:"size" json:"size"` + Author string `bson:"author" json:"author"` + URL string `bson:"url,omitempty" json:"url,omitempty"` +} + +// Upload adds a file to the space. +func Upload(ctx context.Context, name, mimeType, author string, size int64, input io.Reader) (File, error) { + if name == "" { + date := time.Now().UTC().Format("Jan 02 2006 15:04:05 MST") + name = "Unnamed file (" + date + ")" + } + + if mimeType == "" { + mimeType = "binary/octet-stream" + } + + id := makeFileID() + + path, err := store.UploadFile(ctx, "files", id, mimeType, input, size) + if err != nil { + return File{}, err + } + + file := File{ + ID: id, + Time: time.Now(), + Public: false, + Author: author, + Name: name, + MimeType: mimeType, + Size: size, + URL: store.URLFromPath(path), + } + + err = fileCollection.Insert(file) + if err != nil { + return File{}, err + } + + return file, nil +} + +// FindID finds a file by ID +func FindID(id string) (File, error) { + file := File{} + + err := fileCollection.FindId(id).One(&file) + if err != nil { + return File{}, err + } + + return file, nil +} + +// ListFiles lists files according to the standard lookup. By default it's just the author's own files, +// but if `public` is true it will alos include files made public by other authors. If `mimeTypes` contains +// any, it will limit the results to that. +func ListFiles(author string, public bool, mimeTypes []string) ([]File, error) { + query := bson.M{} + + if public { + query["$or"] = []bson.M{ + bson.M{"author": author}, + bson.M{"public": true}, + } + } else { + query["author"] = author + } + + if len(mimeTypes) > 0 { + query["mimeTypes"] = bson.M{"$in": mimeTypes} + } + + return listFiles(query) +} + +func listFiles(query interface{}) ([]File, error) { + list := make([]File, 0, 32) + + err := fileCollection.Find(query).All(&list) + if err != nil { + return nil, err + } + + return list, nil +} + +// makeFileID makes a random file ID that's 32 characters long +func makeFileID() string { + result := "F" + strconv.FormatInt(time.Now().UnixNano(), 36) + offset := 0 + data := make([]byte, 32) + + rand.Read(data) + for len(result) < 32 { + result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) + offset += 8 + + if offset >= 32 { + rand.Read(data) + offset = 0 + } + } + + return result[:32] +} + +func init() { + store.HandleInit(func(db *mgo.Database) { + fileCollection = db.C("file.headers") + + fileCollection.EnsureIndexKey("author") + }) +}