forked from tinode/chat
/
filesys.go
166 lines (139 loc) · 4.31 KB
/
filesys.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
// Package fs implements github.com/tinode/chat/server/media interface by storing media objects in a single
// directory in the file system.
// This module won't perform well with tens of thousand of files because it stores all files in a single directory.
package fs
import (
"encoding/json"
"errors"
"io"
"log"
"mime"
"os"
"path/filepath"
"github.com/tinode/chat/server/media"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
const (
defaultServeURL = "/v0/file/s/"
handlerName = "fs"
)
type configType struct {
FileUploadDirectory string `json:"upload_dir"`
ServeURL string `json:"serve_url"`
}
type fshandler struct {
// In case of a cluster fileUploadLocation must be accessible to all cluster members.
fileUploadLocation string
serveURL string
}
func (fh *fshandler) Init(jsconf string) error {
var err error
var config configType
if err = json.Unmarshal([]byte(jsconf), &config); err != nil {
return errors.New("failed to parse config: " + err.Error())
}
fh.fileUploadLocation = config.FileUploadDirectory
if fh.fileUploadLocation == "" {
return errors.New("missing upload location")
}
fh.serveURL = config.ServeURL
if fh.serveURL == "" {
fh.serveURL = defaultServeURL
}
// Make sure the upload directory exists.
return os.MkdirAll(fh.fileUploadLocation, 0777)
}
// Redirect is used when one wants to serve files from a different external server.
func (fshandler) Redirect(method, url string) (string, error) {
// This handler does not use redirects.
return "", nil
}
// Upload processes request for file upload. The file is given as io.Reader.
func (fh *fshandler) Upload(fdef *types.FileDef, file io.ReadSeeker) (string, error) {
// FIXME: create two-three levels of nested directories. Serving from a single directory
// with tens of thousands of files in it will not perform well.
// Generate a unique file name and attach it to path. Using base32 instead of base64 to avoid possible
// file name collisions on Windows due to case-insensitive file names there.
fdef.Location = filepath.Join(fh.fileUploadLocation, fdef.Uid().String32())
outfile, err := os.Create(fdef.Location)
if err != nil {
log.Println("Upload: failed to create file", fdef.Location, err)
return "", err
}
if err = store.Files.StartUpload(fdef); err != nil {
outfile.Close()
os.Remove(fdef.Location)
log.Println("failed to create file record", fdef.Id, err)
return "", err
}
size, err := io.Copy(outfile, file)
outfile.Close()
if err != nil {
store.Files.FinishUpload(fdef.Id, false, 0)
os.Remove(fdef.Location)
return "", err
}
fdef, err = store.Files.FinishUpload(fdef.Id, true, size)
if err != nil {
os.Remove(fdef.Location)
return "", err
}
fname := fdef.Id
ext, _ := mime.ExtensionsByType(fdef.MimeType)
if len(ext) > 0 {
fname += ext[0]
}
return fh.serveURL + fname, nil
}
// Download processes request for file download.
// The returned ReadSeekCloser must be closed after use.
func (fh *fshandler) Download(url string) (*types.FileDef, media.ReadSeekCloser, error) {
fid := fh.GetIdFromUrl(url)
if fid.IsZero() {
return nil, nil, types.ErrNotFound
}
fd, err := fh.getFileRecord(fid)
if err != nil {
log.Println("Download: file not found", fid)
return nil, nil, err
}
file, err := os.Open(fd.Location)
if err != nil {
if os.IsNotExist(err) {
// If the file is not found, send 404 instead of the default 500
err = types.ErrNotFound
}
return nil, nil, err
}
return fd, file, nil
}
// Delete deletes files from storage by provided slice of locations.
func (fh *fshandler) Delete(locations []string) error {
for _, loc := range locations {
if err, _ := os.Remove(loc).(*os.PathError); err != nil {
if err != os.ErrNotExist {
log.Println("fs: error deleting file", loc, err)
}
}
}
return nil
}
// GetIdFromUrl converts an attahment URL to a file UID.
func (fh *fshandler) GetIdFromUrl(url string) types.Uid {
return media.GetIdFromUrl(url, fh.serveURL)
}
// getFileRecord given file ID reads file record from the database.
func (fh *fshandler) getFileRecord(fid types.Uid) (*types.FileDef, error) {
fd, err := store.Files.Get(fid.String())
if err != nil {
return nil, err
}
if fd == nil {
return nil, types.ErrNotFound
}
return fd, nil
}
func init() {
store.RegisterMediaHandler(handlerName, &fshandler{})
}