-
Notifications
You must be signed in to change notification settings - Fork 6
/
upload.go
231 lines (194 loc) · 5.65 KB
/
upload.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
package coquelicot
import (
"crypto/md5"
"encoding/hex"
"errors"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
)
// Error incomplete returned by uploader when loaded non-last chunk.
var incomplete = errors.New("incomplete")
// Structure describes the state of the original file.
type originalFile struct {
BaseMime string
Filepath string
Filename string
Size int64
}
func (ofile *originalFile) Ext() string {
return strings.ToLower(filepath.Ext(ofile.Filename))
}
// Downloading files from the received request.
// The root directory of storage, storage, used to temporarily store chunks.
// Returns an array of the original files and error.
// If you load a portion of the file, chunk, it will be stored in err error incomplete,
// and in an array of a single file. File size will fit the current size.
func process(req *http.Request, storage string) ([]*originalFile, error) {
meta, err := parseMeta(req)
if err != nil {
return nil, err
}
body, err := newBody(req.Header.Get("X-File"), req.Body)
if err != nil {
return nil, err
}
up := &uploader{Root: storage, Meta: meta, body: body}
files, err := up.SaveFiles()
if err == incomplete {
return files, err
}
if err != nil {
return nil, err
}
return files, nil
}
// Upload manager.
type uploader struct {
Root string
Meta *meta
body *body
}
// Function SaveFiles sequentially loads the original files or chunk's.
func (up *uploader) SaveFiles() ([]*originalFile, error) {
files := make([]*originalFile, 0)
for {
ofile, err := up.SaveFile()
if err == io.EOF {
break
}
if err == incomplete {
files = append(files, ofile)
return files, err
}
if err != nil {
return nil, err
}
files = append(files, ofile)
}
return files, nil
}
// Function loads one or download the original file chunk.
// Asks for the starting position in the body of the request to read the next file.
// Asks for a temporary file.
// Writes data from the request body into a temporary file.
// Specifies the size of the resulting temporary file.
// If the query specified header Content-Range,
// and the size of the resulting file does not match, it returns an error incomplete.
// Otherwise, defines the basic mime type, and returns the original file.
func (up *uploader) SaveFile() (*originalFile, error) {
body, filename, err := up.Reader()
if err != nil {
return nil, err
}
temp_file, err := up.tempFile()
if err != nil {
return nil, err
}
defer temp_file.Close()
if err = up.Write(temp_file, body); err != nil {
return nil, err
}
fi, err := temp_file.Stat()
if err != nil {
return nil, err
}
ofile := &originalFile{Filename: filename, Filepath: temp_file.Name(), Size: fi.Size()}
if up.Meta.Range != nil && ofile.Size != up.Meta.Range.Size {
return ofile, incomplete
}
ofile.BaseMime, err = identifyMime(ofile.Filepath)
if err != nil {
return nil, err
}
return ofile, nil
}
// Returns the reader to read the file or chunk of request body and the original file name.
// If the request header Content-Type is multipart/form-data, returns the next copy part.
// If all of part read the case of binary loading read the request body, an error is returned io.EOF.
func (up *uploader) Reader() (io.Reader, string, error) {
if up.Meta.MediaType == "multipart/form-data" {
if up.body.MR == nil {
up.body.MR = multipart.NewReader(up.body.body, up.Meta.Boundary)
}
for {
part, err := up.body.MR.NextPart()
if err != nil {
return nil, "", err
}
if part.FormName() == "files[]" {
return part, part.FileName(), nil
}
}
}
if !up.body.Available {
return nil, "", io.EOF
}
up.body.Available = false
return up.body.body, up.Meta.Filename, nil
}
// Returns a temporary file to download the file or resume chunk.
func (up *uploader) tempFile() (*os.File, error) {
if up.Meta.Range == nil {
return tempFile()
}
return tempFileChunks(up.Meta.Range.Start, up.Root, up.Meta.UploadSid, up.Meta.Filename)
}
// Returns the newly created temporary file.
func tempFile() (*os.File, error) {
return ioutil.TempFile(os.TempDir(), "coquelicot")
}
// Returns a temporary file to download chunk.
// To calculate a unique file name used cookie named coquelicot and the original file name.
// File located in the directory chunks storage root directory.
// Before returning the file pointer is shifted by the value of offset,
// in a situation where the pieces are loaded from the second to the last.
func tempFileChunks(offset int64, storage, upload_sid, user_filename string) (*os.File, error) {
hasher := md5.New()
hasher.Write([]byte(upload_sid + user_filename))
filename := hex.EncodeToString(hasher.Sum(nil))
path := filepath.Join(storage, "chunks")
err := os.MkdirAll(path, 0755)
if err != nil {
return nil, err
}
file, err := os.OpenFile(filepath.Join(path, filename), os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
return nil, err
}
if _, err = file.Seek(offset, 0); err != nil {
return nil, err
}
return file, nil
}
// The function writes a temporary file value from reader.
func (up *uploader) Write(temp_file *os.File, body io.Reader) error {
var err error
if up.Meta.Range == nil {
_, err = io.Copy(temp_file, body)
} else {
chunk_size := up.Meta.Range.End - up.Meta.Range.Start + 1
_, err = io.CopyN(temp_file, body, chunk_size)
}
return err
}
// identifyMine gets base mime type.
func identifyMime(file string) (string, error) {
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
// DetectContentType reads at most the first 512 bytes
buf := make([]byte, 512)
_, err = f.Read(buf)
if err != nil {
return "", err
}
mime := strings.Split(http.DetectContentType(buf), "/")[0]
return mime, nil
}