This repository has been archived by the owner on Feb 9, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstorage.go
334 lines (294 loc) · 9.73 KB
/
storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
package dhelpers
import (
"bytes"
"encoding/binary"
"errors"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"net/http"
"context"
"github.com/bwmarrin/discordgo"
"github.com/kennygrant/sanitize"
"github.com/minio/minio-go"
"github.com/satori/go.uuid"
"gitlab.com/Cacophony/dhelpers/cache"
"gitlab.com/Cacophony/dhelpers/models"
"gitlab.com/Cacophony/dhelpers/mongo"
"gitlab.com/Cacophony/dhelpers/state"
)
// TODO test all database functions
// TODO: watch cache folder size
// AddFileMetadata defines possible metadta for new objects
type AddFileMetadata struct {
Filename string // the actual file name, can be empty
ChannelID string // the source channel ID, can be empty, but should be set if possible
UserID string // the source user ID, can be empty, but should be set if possible
GuildID string // the source guild ID, can be empty but should be set if possible, will be set automatically if ChannelID has been set
AdditionalMetadata map[string]string // additional metadata attached to the object
}
// AddFile stores a file
// name : the name of the new object, can be empty to generate an unique name
// data : the file data
// metadata : metadata attached to the object
// source : the source name for the file, for example the module name, can not be empty
// public : if true file will be available via the website proxy
// TODO: prevent duplicates
func AddFile(ctx context.Context, name string, data []byte, metadata AddFileMetadata, source string, public bool) (objectName string, err error) {
// check if source is set
if source == "" {
return "", errors.New("source can not be empty")
}
// TODO: check if user is allowed to upload files
// set new object name
objectName = name
if objectName == "" {
var fileUUID uuid.UUID
// generate unique filename
fileUUID, err = uuid.NewV4()
if err != nil {
return "", err
}
objectName = fileUUID.String()
}
// retrieve guildID if channelID is set
guildID := metadata.GuildID
if metadata.ChannelID != "" {
var channel *discordgo.Channel
channel, err = state.Channel(metadata.ChannelID)
LogError(err)
if err == nil {
guildID = channel.GuildID
}
}
// get filetype
filetype := http.DetectContentType(data)
// get filesize
filesize := binary.Size(data)
// update metadata
if metadata.AdditionalMetadata == nil {
metadata.AdditionalMetadata = make(map[string]string)
}
metadata.AdditionalMetadata["filename"] = metadata.Filename
metadata.AdditionalMetadata["userid"] = metadata.UserID
metadata.AdditionalMetadata["guildid"] = guildID
metadata.AdditionalMetadata["channelid"] = metadata.ChannelID
metadata.AdditionalMetadata["source"] = source
metadata.AdditionalMetadata["mimetype"] = filetype
metadata.AdditionalMetadata["filesize"] = strconv.Itoa(filesize)
metadata.AdditionalMetadata["public"] = "no"
if public {
metadata.AdditionalMetadata["public"] = "yes"
}
// upload file
err = uploadFile(objectName, data, metadata.AdditionalMetadata)
if err != nil {
return "", err
}
// store in database
err = models.StorageRepository.Update(
ctx,
map[string]string{"objectname": objectName},
models.StorageEntry{
ObjectName: objectName,
ObjectNameHash: GetMD5Hash(objectName),
UploadDate: time.Now(),
Filename: metadata.Filename,
UserID: metadata.UserID,
GuildID: guildID,
ChannelID: metadata.ChannelID,
Source: source,
MimeType: filetype,
Filesize: filesize,
Public: public,
Metadata: metadata.AdditionalMetadata,
},
)
if err != nil {
return "", err
}
// TODO: warm up cache for public files
cache.GetLogger().WithField("module", "storage").Infof(
"stored #%s for %s (%+v)",
objectName, source, metadata,
)
// return new objectName
return objectName, nil
}
// RetrieveFileInformation retrieves information about a file
// objectName : the name of the file to retrieve
func RetrieveFileInformation(ctx context.Context, objectName string) (info models.StorageEntry, err error) {
err = models.StorageRepository.FindOne(
ctx,
map[string]string{"objectname": objectName},
&info,
)
return info, err
}
// RetrieveFile retrieves a file
// objectName : the name of the file to retrieve
func RetrieveFile(ctx context.Context, objectName string) (data []byte, err error) {
// Increase MongoDB RetrievedCount
go func() {
defer RecoverLog()
goErr := models.StorageRepository.Update(
ctx,
map[string]string{"objectname": objectName},
map[string]map[string]int{"$inc": {"retrievedcount": 1}},
)
if goErr != nil && !(goErr == mongo.ErrNotFound) {
CheckErr(goErr)
}
}()
data = getBucketCache(objectName)
if data != nil {
cache.GetLogger().WithField("module", "storage").Infof("retrieving " + objectName + " from minio cache")
return data, nil
}
cache.GetLogger().WithField("module", "storage").Infof("retrieving " + objectName + " from minio storage")
// retrieve the object
minioObject, err := cache.GetMinio().GetObject(getBucket(), sanitize.BaseName(objectName), minio.GetObjectOptions{})
if err != nil {
if strings.Contains(err.Error(), "Please reduce your request rate.") {
cache.GetLogger().WithField("module", "storage").Infof("object storage ratelimited, waiting for one second, then retrying")
time.Sleep(1 * time.Second)
return RetrieveFile(ctx, objectName)
}
if strings.Contains(err.Error(), "net/http") || strings.Contains(err.Error(), "timeout") {
cache.GetLogger().WithField("module", "storage").Infof("network error retrieving, waiting for one second, then retrying")
time.Sleep(1 * time.Second)
return RetrieveFile(ctx, objectName)
}
return data, err
}
// read the object into a byte slice
data, err = ioutil.ReadAll(minioObject)
if err != nil {
return data, err
}
go func() {
defer RecoverLog()
cache.GetLogger().WithField("module", "storage").Infof("caching " + objectName + " into minio cache")
err := setBucketCache(objectName, data)
CheckErr(err)
}()
return data, nil
}
// RetrieveFileByHash retrieves a file by the object name md5 hash
// hash : the md5 hash
func RetrieveFileByHash(ctx context.Context, hash string) (filename, filetype string, data []byte, err error) {
var entryBucket models.StorageEntry
err = models.StorageRepository.FindOne(
ctx,
map[string]string{"objectnamehash": hash},
&entryBucket,
)
if err != nil && !(err == mongo.ErrNotFound) {
return "", "", nil, err
}
data, err = RetrieveFile(ctx, entryBucket.ObjectName)
if err != nil {
return "", "", nil, err
}
return entryBucket.Filename, entryBucket.MimeType, data, nil
}
// RetrieveFilesByAdditionalObjectMetadata retrieves files by additional object metadta
// currently supported file sources: custom commands
// hash : the md5 hash
func RetrieveFilesByAdditionalObjectMetadata(ctx context.Context, key, value string) (objectNames []string, err error) {
var entryBucket []models.StorageEntry
err = models.StorageRepository.Find(
ctx,
map[string]string{"metadata." + strings.ToLower(key): value},
&entryBucket,
)
if err != nil {
return nil, err
}
objectNames = make([]string, 0)
if len(entryBucket) > 0 {
for _, entry := range entryBucket {
objectNames = append(objectNames, entry.ObjectName)
}
}
if len(objectNames) < 1 {
return nil, errors.New("none matching files found")
}
return objectNames, nil
}
// DeleteFile deletes a file
// objectName : the name of the object
func DeleteFile(ctx context.Context, objectName string) (err error) {
cache.GetLogger().WithField("module", "storage").Infof("deleting " + objectName + " from minio storage")
go func() {
defer RecoverLog()
cache.GetLogger().WithField("module", "storage").Infof("deleting " + objectName + " from minio cache")
goErr := deleteBucketCache(objectName)
CheckErr(goErr)
}()
// delete the object
err = cache.GetMinio().RemoveObject(getBucket(), sanitize.BaseName(objectName))
// delete mongo db entry
go func() {
defer RecoverLog()
goErr := models.StorageRepository.Delete(ctx, map[string]string{"objectname": objectName})
if goErr != nil && !(goErr == mongo.ErrNotFound) {
CheckErr(err)
}
}()
return err
}
// uploads a file to the minio object storage
// objectName : the name of the file to upload
// data : the data for the new object
// metadata : additional metadata attached to the object
// TODO: prevent overwrites
func uploadFile(objectName string, data []byte, metadata map[string]string) (err error) {
options := minio.PutObjectOptions{}
// add content type
options.ContentType = http.DetectContentType(data)
// add metadata
if len(metadata) > 0 {
options.UserMetadata = metadata
}
// upload the data
_, err = cache.GetMinio().PutObject(getBucket(), sanitize.BaseName(objectName), bytes.NewReader(data), -1, options)
return err
}
func getBucketCache(objectName string) (data []byte) {
var err error
if _, err = os.Stat(getObjectPath(objectName)); os.IsNotExist(err) {
return nil
}
data, err = ioutil.ReadFile(getObjectPath(objectName))
if err != nil {
return nil
}
return data
}
func setBucketCache(objectName string, data []byte) (err error) {
if _, err = os.Stat(filepath.Dir(getObjectPath(objectName))); os.IsNotExist(err) {
err = os.MkdirAll(filepath.Dir(getObjectPath(objectName)), os.ModePerm)
if err != nil {
return err
}
}
err = ioutil.WriteFile(getObjectPath(objectName), data, 0644)
return err
}
func deleteBucketCache(objectName string) (err error) {
if _, err = os.Stat(getObjectPath(objectName)); os.IsNotExist(err) {
return nil
}
err = os.Remove(getObjectPath(objectName))
return err
}
func getObjectPath(objectName string) (path string) {
return os.Getenv("S3_CACHE_FOLDER") + "/" + sanitize.BaseName(objectName)
}
func getBucket() (bucket string) {
return os.Getenv("S3_BUCKET")
}