models: Migrated last from model
This commit is contained in:
@@ -1,105 +0,0 @@
|
|||||||
package change
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/model/counter"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var collection *mgo.Collection
|
|
||||||
|
|
||||||
// A Change represents a change in any other model
|
|
||||||
type Change struct {
|
|
||||||
ID int `bson:"_id" json:"id"`
|
|
||||||
Time time.Time `bson:"time" json:"time"`
|
|
||||||
Model string `bson:"model" json:"model"`
|
|
||||||
Op string `bson:"op" json:"op"`
|
|
||||||
Author string `bson:"author,omitempty" json:"author,omitempty"`
|
|
||||||
ObjectID string `bson:"objectId,omitempty" json:"objectId,omitempty"`
|
|
||||||
Data interface{} `bson:"data,omitempty" json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublicModels lists which models can be listed in bulk by anyone.
|
|
||||||
var PublicModels = []string{
|
|
||||||
"Character",
|
|
||||||
"Log",
|
|
||||||
"Post",
|
|
||||||
}
|
|
||||||
|
|
||||||
var submitMutex sync.Mutex
|
|
||||||
|
|
||||||
// Submit submits a change to the history.
|
|
||||||
func Submit(model, op, author, objectID string, data interface{}) (Change, error) {
|
|
||||||
submitMutex.Lock()
|
|
||||||
defer submitMutex.Unlock()
|
|
||||||
|
|
||||||
index, err := counter.Next("auto_increment", "Change")
|
|
||||||
if err != nil {
|
|
||||||
return Change{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
change := Change{
|
|
||||||
ID: index,
|
|
||||||
Time: time.Now(),
|
|
||||||
Model: model,
|
|
||||||
Op: op,
|
|
||||||
Author: author,
|
|
||||||
ObjectID: objectID,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
Timeline.push(change)
|
|
||||||
|
|
||||||
err = collection.Insert(&change)
|
|
||||||
if err != nil {
|
|
||||||
return Change{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return change, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindID gets a change by ID
|
|
||||||
func FindID(id int) (Change, error) {
|
|
||||||
change := Change{}
|
|
||||||
err := collection.FindId(id).One(&change)
|
|
||||||
if err != nil {
|
|
||||||
return Change{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return change, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List gets all changes in ascending order
|
|
||||||
func List() ([]Change, error) {
|
|
||||||
changes := make([]Change, 0, 64)
|
|
||||||
err := collection.Find(nil).Sort("_id").All(&changes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
collection = db.C("common.history")
|
|
||||||
|
|
||||||
collection.EnsureIndexKey("model")
|
|
||||||
collection.EnsureIndexKey("author")
|
|
||||||
collection.EnsureIndexKey("objectId")
|
|
||||||
|
|
||||||
err := collection.EnsureIndex(mgo.Index{
|
|
||||||
Name: "expiry",
|
|
||||||
Key: []string{"time"},
|
|
||||||
ExpireAfter: time.Hour * 336,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package change
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrInvalidIndex is returned when a timeline subscriber tries to get a value
|
|
||||||
// with a negative id
|
|
||||||
var ErrInvalidIndex = errors.New("change.timeline: Invalid ID")
|
|
||||||
|
|
||||||
// ErrWaitRequired is returned when a timeline subscriber requests an entry that
|
|
||||||
// has not yet been pushed
|
|
||||||
var ErrWaitRequired = errors.New("change.timeline: Waiting required")
|
|
||||||
|
|
||||||
// ErrExpired is returned when a timeline subscriber requests a change that happened
|
|
||||||
// too long ago for it to be in the database.
|
|
||||||
var ErrExpired = errors.New("change.timeline: Change has expired")
|
|
||||||
|
|
||||||
type timeline struct {
|
|
||||||
mutex sync.Mutex
|
|
||||||
firstIndex int64
|
|
||||||
currentID int64
|
|
||||||
cache []Change
|
|
||||||
notifyCh chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (timeline *timeline) Get(ctx context.Context, id int64) (Change, error) {
|
|
||||||
change, err := timeline.get(id, false)
|
|
||||||
if err == ErrWaitRequired {
|
|
||||||
if err := timeline.wait(ctx); err != nil {
|
|
||||||
return Change{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeline.get(id, true)
|
|
||||||
} else if err == mgo.ErrNotFound {
|
|
||||||
return Change{}, ErrExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
return change, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (timeline *timeline) push(change Change) {
|
|
||||||
timeline.mutex.Lock()
|
|
||||||
timeline.cache = append(timeline.cache, change)
|
|
||||||
if len(timeline.cache) > 128 {
|
|
||||||
copy(timeline.cache, timeline.cache[32:])
|
|
||||||
timeline.cache = timeline.cache[:len(timeline.cache)-32]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update the currentID if this is more recent than the existing ones
|
|
||||||
if int64(change.ID) > timeline.currentID {
|
|
||||||
timeline.currentID = int64(change.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeline.notifyCh != nil {
|
|
||||||
close(timeline.notifyCh)
|
|
||||||
timeline.notifyCh = nil
|
|
||||||
}
|
|
||||||
timeline.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (timeline *timeline) get(id int64, hasWaited bool) (Change, error) {
|
|
||||||
if id < 0 {
|
|
||||||
return Change{}, ErrInvalidIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.mutex.Lock()
|
|
||||||
if !hasWaited && id > timeline.currentID {
|
|
||||||
timeline.mutex.Unlock()
|
|
||||||
return Change{}, ErrWaitRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, change := range timeline.cache {
|
|
||||||
if change.ID == int(id) {
|
|
||||||
timeline.mutex.Unlock()
|
|
||||||
return change, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
timeline.mutex.Unlock()
|
|
||||||
|
|
||||||
return FindID(int(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (timeline *timeline) wait(ctx context.Context) error {
|
|
||||||
timeline.mutex.Lock()
|
|
||||||
if timeline.notifyCh == nil {
|
|
||||||
timeline.notifyCh = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := timeline.notifyCh
|
|
||||||
timeline.mutex.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-ch:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeline has operations for subscribing to changes in real time
|
|
||||||
var Timeline = &timeline{}
|
|
||||||
|
|
||||||
// InitializeTimeline fills in the timeline with existing entries
|
|
||||||
func InitializeTimeline() {
|
|
||||||
changes, err := List()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, change := range changes {
|
|
||||||
Timeline.push(change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"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"`
|
|
||||||
Kind string `bson:"kind" json:"kind"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit edits the file, changing up to both the two mutable properties
|
|
||||||
func (file *File) Edit(name *string, public *bool) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
changedFile := *file
|
|
||||||
|
|
||||||
if name != nil && *name != file.Name {
|
|
||||||
changes["name"] = *name
|
|
||||||
changedFile.Name = *name
|
|
||||||
}
|
|
||||||
if public != nil && *public != file.Public {
|
|
||||||
changes["public"] = *public
|
|
||||||
changedFile.Public = *public
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := fileCollection.UpdateId(file.ID, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*file = changedFile
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the file information from the database, and deletes the file.
|
|
||||||
func (file *File) Delete() error {
|
|
||||||
err := fileCollection.RemoveId(file.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.Kind == "upload" {
|
|
||||||
err = store.RemoveFile("files", file.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.URL = ""
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert manually inserts file information into the database. This should never, ever be API accessible
|
|
||||||
func Insert(name, kind, mimeType, author string, time time.Time, size int64, url string) (File, error) {
|
|
||||||
file := File{
|
|
||||||
ID: makeFileID(),
|
|
||||||
Kind: kind,
|
|
||||||
Time: time,
|
|
||||||
Public: false,
|
|
||||||
Author: author,
|
|
||||||
Name: name,
|
|
||||||
MimeType: mimeType,
|
|
||||||
Size: size,
|
|
||||||
URL: url,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := fileCollection.Insert(file)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload adds a file to the space.
|
|
||||||
func Upload(ctx context.Context, name, mimeType, author string, size int64, input io.Reader) (File, error) {
|
|
||||||
if !allowdMimeTypes[mimeType] {
|
|
||||||
return File{}, errors.New("File type not allowed:" + mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
Kind: "upload",
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindName finds a file by kind, name and author
|
|
||||||
func FindName(kind string, name string, author string) (File, error) {
|
|
||||||
query := bson.M{"kind": kind, "name": name, "author": author}
|
|
||||||
file := File{}
|
|
||||||
|
|
||||||
err := fileCollection.Find(query).One(&file)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 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. If `author` is empty, it will only list public files
|
|
||||||
func List(author string, public *bool, mimeTypes []string) ([]File, error) {
|
|
||||||
query := bson.M{}
|
|
||||||
|
|
||||||
if author != "" {
|
|
||||||
if public != nil {
|
|
||||||
if *public == false {
|
|
||||||
query["author"] = author
|
|
||||||
}
|
|
||||||
|
|
||||||
query["public"] = *public
|
|
||||||
} else {
|
|
||||||
query["author"] = author
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if public != nil && *public == false {
|
|
||||||
return nil, errors.New("You cannot")
|
|
||||||
}
|
|
||||||
|
|
||||||
query["public"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
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).Sort("-time").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")
|
|
||||||
fileCollection.EnsureIndexKey("public")
|
|
||||||
fileCollection.EnsureIndexKey("kind", "name", "author")
|
|
||||||
fileCollection.EnsureIndexKey("author", "public")
|
|
||||||
fileCollection.EnsureIndexKey("kind")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowdMimeTypes = map[string]bool{
|
|
||||||
"": false,
|
|
||||||
"image/jpeg": true,
|
|
||||||
"image/png": true,
|
|
||||||
"image/gif": true,
|
|
||||||
"text/plain": true,
|
|
||||||
"application/json": true,
|
|
||||||
"application/pdf": false,
|
|
||||||
"binary/octet-stream": false,
|
|
||||||
"video/mp4": false,
|
|
||||||
"audio/mp3": false,
|
|
||||||
}
|
|
||||||
@@ -72,6 +72,8 @@ var allowdMimeTypes = map[string]bool{
|
|||||||
"image/jpeg": true,
|
"image/jpeg": true,
|
||||||
"image/png": true,
|
"image/png": true,
|
||||||
"image/gif": true,
|
"image/gif": true,
|
||||||
|
"image/tiff": true,
|
||||||
|
"image/tga": true,
|
||||||
"text/plain": true,
|
"text/plain": true,
|
||||||
"application/json": true,
|
"application/json": true,
|
||||||
"application/pdf": false,
|
"application/pdf": false,
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ import (
|
|||||||
func FindID(id string) (models.File, error) {
|
func FindID(id string) (models.File, error) {
|
||||||
return find(bson.M{"_id": id})
|
return find(bson.M{"_id": id})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindName finds a file by ID
|
||||||
|
func FindName(kind, name, author string) (models.File, error) {
|
||||||
|
return find(bson.M{"name": name, "kind": kind, "author": author})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Insert manually inserts file information into the database. This should never, ever be HTTP API accessible
|
||||||
|
func Insert(name, kind, mimeType, author string, time time.Time, size int64, url string) (models.File, error) {
|
||||||
|
file := models.File{
|
||||||
|
ID: makeID(),
|
||||||
|
Kind: kind,
|
||||||
|
Time: time,
|
||||||
|
Public: false,
|
||||||
|
Author: author,
|
||||||
|
Name: name,
|
||||||
|
MimeType: mimeType,
|
||||||
|
Size: size,
|
||||||
|
URL: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := collection.Insert(file)
|
||||||
|
if err != nil {
|
||||||
|
return models.File{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/internal/store"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upload adds a file to the space.
|
||||||
|
func Upload(ctx context.Context, name, mimeType, author string, size int64, input io.Reader) (models.File, error) {
|
||||||
|
if !allowdMimeTypes[mimeType] {
|
||||||
|
return models.File{}, errors.New("File type not allowed:" + mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := makeID()
|
||||||
|
|
||||||
|
path, err := store.UploadFile(ctx, "files", id, mimeType, input, size)
|
||||||
|
if err != nil {
|
||||||
|
return models.File{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := models.File{
|
||||||
|
ID: id,
|
||||||
|
Kind: "upload",
|
||||||
|
Time: time.Now(),
|
||||||
|
Public: false,
|
||||||
|
Author: author,
|
||||||
|
Name: name,
|
||||||
|
MimeType: mimeType,
|
||||||
|
Size: size,
|
||||||
|
URL: store.URLFromPath(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.Insert(file)
|
||||||
|
if err != nil {
|
||||||
|
return models.File{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user