-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmemfile.go
498 lines (448 loc) · 15.1 KB
/
memfile.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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
package fs
import (
"archive/zip"
"bytes"
"context"
"encoding/gob"
"encoding/json"
"encoding/xml"
"fmt"
"io"
iofs "io/fs"
"strings"
"time"
"github.com/ungerik/go-fs/fsimpl"
)
// Ensure MemFile implements interfaces
var (
_ FileReader = MemFile{}
_ io.Writer = &MemFile{}
_ io.WriterTo = MemFile{}
_ io.ReaderAt = MemFile{}
// _ json.Marshaler = MemFile{}
// _ json.Unmarshaler = &MemFile{}
_ gob.GobEncoder = MemFile{}
_ gob.GobDecoder = &MemFile{}
_ fmt.Stringer = MemFile{}
)
// MemFile implements FileReader with a filename and an in memory byte slice.
// It exposes FileName and FileData as exported struct fields to emphasize
// its simple nature as just an wrapper around a name and some bytes.
//
// As a small an simple struct MemFile is usually passed by value.
// This is why NewMemFile does not return a pointer.
//
// Note that the ReadAll and ReadAllContext methods return FileData
// directly without copying it to optimized performance.
// So be careful when modifying the FileData bytes of a MemFile.
//
// MemFile implements the following interfaces:
// - FileReader
// - io.Writer
// - io.WriterTo
// - io.ReaderAt
// - json.Marshaler
// - json.Unmarshaler
// - gob.GobEncoder
// - gob.GobDecoder
// - fmt.Stringer
type MemFile struct {
FileName string `json:"filename"`
FileData []byte `json:"data,omitempty"`
}
// NewMemFile returns a new MemFile
func NewMemFile(name string, data []byte) MemFile {
return MemFile{FileName: name, FileData: data}
}
// NewMemFileWriteJSON returns a new MemFile with the input mashalled to JSON as FileData.
// Any indent arguments will be concanated and used as JSON line indentation.
//
// Returns a wrapped ErrMarshalJSON when the marshalling failed.
func NewMemFileWriteJSON(name string, input any, indent ...string) (MemFile, error) {
var (
data []byte
err error
)
if len(indent) == 0 {
data, err = json.Marshal(input)
} else {
data, err = json.MarshalIndent(input, "", strings.Join(indent, ""))
}
if err != nil {
return MemFile{}, fmt.Errorf("%w because: %w", ErrMarshalJSON, err)
}
return MemFile{FileName: name, FileData: data}, nil
}
// NewMemFileWriteXML returns a new MemFile with the input mashalled to XML as FileData.
// Any indent arguments will be concanated and used as XML line indentation.
//
// Returns a wrapped ErrMarshalXML when the marshalling failed.
func NewMemFileWriteXML(name string, input any, indent ...string) (MemFile, error) {
var (
data []byte
err error
)
if len(indent) == 0 {
data, err = xml.Marshal(input)
} else {
data, err = xml.MarshalIndent(input, "", strings.Join(indent, ""))
}
if err != nil {
return MemFile{}, fmt.Errorf("%w because: %w", ErrMarshalXML, err)
}
return MemFile{FileName: name, FileData: append([]byte(xml.Header), data...)}, nil
}
// ReadMemFile returns a new MemFile with name and data from fileReader.
// If the passed fileReader is a MemFile then
// its FileData is used directly without copying it.
func ReadMemFile(ctx context.Context, fileReader FileReader) (MemFile, error) {
data, err := fileReader.ReadAllContext(ctx) // Does not copy in case of fileReader.(MemFile)
if err != nil {
return MemFile{}, fmt.Errorf("ReadMemFile: error reading from FileReader: %w", err)
}
return MemFile{FileName: fileReader.Name(), FileData: data}, nil
}
// ReadMemFileRename returns a new MemFile with the data from fileReader and the passed name.
// If the passed fileReader is a MemFile then
// its FileData is used directly without copying it.
func ReadMemFileRename(ctx context.Context, fileReader FileReader, name string) (MemFile, error) {
data, err := fileReader.ReadAllContext(ctx) // Does not copy in case of fileReader.(MemFile)
if err != nil {
return MemFile{}, fmt.Errorf("ReadMemFileRename: error reading from FileReader: %w", err)
}
return MemFile{FileName: name, FileData: data}, nil
}
// ReadAllMemFile returns a new MemFile with the data
// from ReadAllContext(r) and the passed name.
// It reads all data from r until EOF is reached,
// another error is returned, or the context got canceled.
func ReadAllMemFile(ctx context.Context, r io.Reader, name string) (MemFile, error) {
data, err := ReadAllContext(ctx, r)
if err != nil {
return MemFile{}, fmt.Errorf("ReadAllMemFile: error reading from io.Reader: %w", err)
}
return MemFile{FileName: name, FileData: data}, nil
}
// String returns the metadata of the file formatted as a string.
// String implements the fmt.Stringer interface.
func (f MemFile) String() string {
return fmt.Sprintf("MemFile{name: `%s`, size: %d}", f.FileName, len(f.FileData))
}
// PrintForCallStack prints the metadata of the file
// for call stack errors.
func (f MemFile) PrintForCallStack(w io.Writer) {
_, _ = io.WriteString(w, f.String())
}
// Name returns the name of the file.
// If FileName contains a slash or backslash
// then only the part after it will be returned.
func (f MemFile) Name() string {
if i := strings.LastIndexAny(f.FileName, `/\`); i >= 0 {
return f.FileName[i+1:]
}
return f.FileName
}
// WithName returns a MemFile with the passed name
// and the same shared data as the original MemFile.
func (f MemFile) WithName(name string) MemFile {
return MemFile{FileName: name, FileData: f.FileData}
}
// WithData returns a MemFile with the passed data
// and the same name as the original MemFile.
func (f MemFile) WithData(data []byte) MemFile {
return MemFile{FileName: f.FileName, FileData: data}
}
// Ext returns the extension of file name including the point, or an empty string.
func (f MemFile) Ext() string {
return fsimpl.Ext(f.FileName, "")
}
// ExtLower returns the lower case extension of the FileName including the point, or an empty string.
func (f MemFile) ExtLower() string {
return strings.ToLower(f.Ext())
}
// LocalPath always returns an empty string for a MemFile.
func (MemFile) LocalPath() string {
return ""
}
// Size returns the size of the file
func (f MemFile) Size() int64 {
return int64(len(f.FileData))
}
// Exists returns true if the MemFile has non empty FileName.
// It's valid to call this method on a nil pointer,
// will return false in this case.
func (f MemFile) Exists() bool {
return f.FileName != ""
}
// CheckExists return an ErrDoesNotExist error
// if the file does not exist.
func (f MemFile) CheckExists() error {
if !f.Exists() {
return NewErrDoesNotExistFileReader(f)
}
return nil
}
// IsDir always returns false for a MemFile.
func (MemFile) IsDir() bool {
return false
}
// CheckIsDir always returns ErrIsNotDirectory.
func (f MemFile) CheckIsDir() error {
return NewErrIsNotDirectory(f)
}
// ContentHash returns the DefaultContentHash for the file.
func (f MemFile) ContentHash() (string, error) {
return f.ContentHashContext(context.Background())
}
// ContentHashContext returns the DefaultContentHash for the file.
func (f MemFile) ContentHashContext(ctx context.Context) (string, error) {
return DefaultContentHash(ctx, bytes.NewReader(f.FileData))
}
// ReadAll returns the FileData without copying it.
func (f MemFile) ReadAll() (data []byte, err error) {
return f.FileData, nil
}
// ReadAllContext returns the FileData without copying it.
func (f MemFile) ReadAllContext(ctx context.Context) (data []byte, err error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
return f.FileData, nil
}
// ReadAllContentHash returns the FileData without copying it
// together with the DefaultContentHash.
func (f MemFile) ReadAllContentHash(ctx context.Context) (data []byte, hash string, err error) {
hash, err = DefaultContentHash(ctx, bytes.NewReader(f.FileData))
if err != nil {
return nil, "", err
}
return f.FileData, hash, nil
}
// ReadAllString returns the FileData as string.
func (f MemFile) ReadAllString() (string, error) {
return string(f.FileData), nil
}
// ReadAllStringContext returns the FileData as string.
func (f MemFile) ReadAllStringContext(ctx context.Context) (string, error) {
if ctx.Err() != nil {
return "", ctx.Err()
}
return string(f.FileData), nil
}
// ReadAt reads len(p) bytes into p starting at offset off in the
// underlying input source. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered.
//
// When ReadAt returns n < len(p), it returns a non-nil error
// explaining why more bytes were not returned. In this respect,
// ReadAt is stricter than Read.
//
// If the n = len(p) bytes returned by ReadAt are at the end of the
// input source, ReadAt returns err == nil.
//
// Clients of ReadAt can execute parallel ReadAt calls on the
// same input source.
//
// ReadAt implements the interface io.ReaderAt.
func (f MemFile) ReadAt(p []byte, off int64) (n int, err error) {
if off >= int64(len(f.FileData)) {
return 0, io.EOF
}
n = copy(p, f.FileData[off:])
if n < len(p) {
return n, fmt.Errorf("could only read %d of %d requested bytes", n, len(p))
}
return n, nil
}
// WriteTo implements the io.WriterTo interface
func (f MemFile) WriteTo(writer io.Writer) (n int64, err error) {
i, err := writer.Write(f.FileData)
return int64(i), err
}
// OpenReader opens the file and returns a io/fs.File that has to be closed after reading
func (f MemFile) OpenReader() (ReadCloser, error) {
return fsimpl.NewReadonlyFileBuffer(f.FileData, memFileInfo{f}), nil
}
// OpenReadSeeker opens the file and returns a ReadSeekCloser.
// Use OpenReader if seeking is not necessary because implementations
// may need additional buffering to support seeking or not support it at all.
func (f MemFile) OpenReadSeeker() (ReadSeekCloser, error) {
return fsimpl.NewReadonlyFileBuffer(f.FileData, memFileInfo{f}), nil
}
// ReadJSON reads and unmarshalles the JSON content of the file to output.
//
// Returns a wrapped ErrUnmarshalJSON when the unmarshalling failed.
func (f MemFile) ReadJSON(ctx context.Context, output any) error {
// Context is passed for identical call signature as other types
if err := ctx.Err(); err != nil {
return err
}
err := json.Unmarshal(f.FileData, output)
if err != nil {
return fmt.Errorf("%w because: %w", ErrUnmarshalJSON, err)
}
return nil
}
// WriteJSON mashalles input to JSON and writes it as the file.
// Any indent arguments will be concanated and used as JSON line indentation.
//
// Returns a wrapped ErrMarshalJSON when the marshalling failed.
func (f *MemFile) WriteJSON(ctx context.Context, input any, indent ...string) (err error) {
// Context is passed for identical call signature as other types
if err = ctx.Err(); err != nil {
return err
}
var data []byte
if len(indent) == 0 {
data, err = json.Marshal(input)
} else {
data, err = json.MarshalIndent(input, "", strings.Join(indent, ""))
}
if err != nil {
return fmt.Errorf("%w because: %w", ErrMarshalJSON, err)
}
f.FileData = data
return nil
}
// ReadXML reads and unmarshalles the XML content of the file to output.
//
// Returns a wrapped ErrUnmarshalXML when the unmarshalling failed.
func (f MemFile) ReadXML(ctx context.Context, output any) error {
// Context is passed for identical call signature as other types
if err := ctx.Err(); err != nil {
return err
}
err := xml.Unmarshal(f.FileData, output)
if err != nil {
return fmt.Errorf("%w because: %w", ErrUnmarshalXML, err)
}
return nil
}
// WriteXML mashalles input to XML and writes it as the file.
// Any indent arguments will be concanated and used as XML line indentation.
//
// Returns a wrapped ErrMarshalXML when the marshalling failed.
func (f *MemFile) WriteXML(ctx context.Context, input any, indent ...string) (err error) {
// Context is passed for identical call signature as other types
if err = ctx.Err(); err != nil {
return err
}
var data []byte
if len(indent) == 0 {
data, err = xml.Marshal(input)
} else {
data, err = xml.MarshalIndent(input, "", strings.Join(indent, ""))
}
if err != nil {
return fmt.Errorf("%w because: %w", ErrMarshalXML, err)
}
f.FileData = append([]byte(xml.Header), data...)
return nil
}
// // MarshalJSON implements the json.Marshaler interface
// func (f MemFile) MarshalJSON() ([]byte, error) {
// encodedData := base64.RawURLEncoding.EncodeToString(f.FileData)
// // fmt.Errorf("%w because: %w", ErrMarshalJSON, err)
// return json.Marshal(map[string]string{f.FileName: encodedData})
// }
// // UnmarshalJSON implements the json.Unmarshaler interface
// func (f *MemFile) UnmarshalJSON(j []byte) error {
// m := make(map[string]string, 1)
// err := json.Unmarshal(j, &m)
// if err != nil {
// return fmt.Errorf("can't unmarshal JSON as MemFile: %w", err)
// }
// if len(m) != 1 {
// return fmt.Errorf("can't unmarshal JSON as MemFile: %d object keys", len(m))
// }
// for fileName, encodedData := range m {
// fileData, err := base64.RawURLEncoding.DecodeString(encodedData)
// if err != nil {
// return fmt.Errorf("can't decode base64 JSON data of MemFile: %w", err)
// }
// f.FileName = fileName
// f.FileData = fileData
// }
// return nil
// }
// GobEncode gob encodes the file name and content,
// implementing encoding/gob.GobEncoder.
func (f MemFile) GobEncode() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, 16+len(f.FileName)+len(f.FileData)))
enc := gob.NewEncoder(buf)
err := enc.Encode(f.FileName)
if err != nil {
return nil, fmt.Errorf("MemFile.GobEncode: error encoding FileName: %w", err)
}
err = enc.Encode(f.FileData)
if err != nil {
return nil, fmt.Errorf("MemFile.GobEncode: error encoding FileData: %w", err)
}
return buf.Bytes(), nil
}
// GobDecode decodes gobBytes file name and content,
// implementing encoding/gob.GobDecoder.
func (f *MemFile) GobDecode(gobBytes []byte) error {
dec := gob.NewDecoder(bytes.NewReader(gobBytes))
err := dec.Decode(&f.FileName)
if err != nil {
return fmt.Errorf("MemFile.GobDecode: error decoding FileName: %w", err)
}
err = dec.Decode(&f.FileData)
if err != nil {
return fmt.Errorf("MemFile.GobDecode: error decoding FileData: %w", err)
}
return nil
}
// Write appends the passed bytes to the FileData,
// implementing the io.Writer interface.
func (f *MemFile) Write(b []byte) (int, error) {
f.FileData = append(f.FileData, b...)
return len(b), nil
}
// Stat returns a io/fs.FileInfo describing the MemFile.
func (f MemFile) Stat() (iofs.FileInfo, error) {
return memFileInfo{f}, nil
}
var _ iofs.FileInfo = memFileInfo{}
// memFileInfo implements io/fs.FileInfo for a MemFile.
//
// Name() is derived from MemFile.
type memFileInfo struct {
MemFile
}
func (i memFileInfo) Mode() iofs.FileMode { return 0666 }
func (i memFileInfo) ModTime() time.Time { return time.Now() }
func (i memFileInfo) IsDir() bool { return false }
func (i memFileInfo) Sys() any { return nil }
// UnzipToMemFiles unzips the passed zipFile as MemFiles.
func UnzipToMemFiles(ctx context.Context, zipFile FileReader) ([]MemFile, error) {
fileReader, err := zipFile.OpenReadSeeker()
if err != nil {
return nil, err
}
defer fileReader.Close()
zipReader, err := zip.NewReader(fileReader, zipFile.Size())
if err != nil {
return nil, err
}
memFiles := make([]MemFile, len(zipReader.File))
for i, zipFile := range zipReader.File {
if strings.HasSuffix(zipFile.Name, "/") {
continue
}
r, err := zipFile.Open()
if err != nil {
return nil, err
}
memFiles[i], err = ReadAllMemFile(ctx, r, zipFile.Name)
if err != nil {
return nil, err
}
err = r.Close()
if err != nil {
return nil, err
}
}
return memFiles, nil
}