From 95da668a2f0f1f4b29abea38cfec853f8dd695f5 Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 3 Dec 2024 10:04:25 +0800 Subject: [PATCH] feat: add mode and mtime --- file.go | 9 ++++++ linkfile.go | 13 ++++++++ multifilereader.go | 42 ++++++++++++++++++++------ multipartfile.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++ readerfile.go | 15 ++++++++++ serialfile.go | 11 ++++++- slicedirectory.go | 19 +++++++++++- webfile.go | 11 +++++++ 8 files changed, 184 insertions(+), 11 deletions(-) diff --git a/file.go b/file.go index bd80bbd..0518ae5 100644 --- a/file.go +++ b/file.go @@ -4,6 +4,7 @@ import ( "errors" "io" "os" + "time" ) var ( @@ -17,6 +18,14 @@ var ( type Node interface { io.Closer + // Mode returns the mode. + // Optional, if unknown/unspecified returns zero. + Mode() os.FileMode + + // ModTime returns the last modification time. If the last + // modification time is unknown/unspecified ModTime returns zero. + ModTime() (mtime time.Time) + // Size returns size of this file (if this file is a directory, total size of // all files stored in the tree should be returned). Some implementations may // choose not to implement this diff --git a/linkfile.go b/linkfile.go index 5269986..8d6757c 100644 --- a/linkfile.go +++ b/linkfile.go @@ -3,17 +3,22 @@ package files import ( "os" "strings" + "time" ) type Symlink struct { Target string + mtime time.Time stat os.FileInfo reader strings.Reader } func NewLinkFile(target string, stat os.FileInfo) File { lf := &Symlink{Target: target, stat: stat} + if stat.ModTime() != (time.Time{}) { + lf.mtime = stat.ModTime() + } lf.reader.Reset(lf.Target) return lf } @@ -30,6 +35,14 @@ func (lf *Symlink) Seek(offset int64, whence int) (int64, error) { return lf.reader.Seek(offset, whence) } +func (lf *Symlink) Mode() os.FileMode { + return os.ModeSymlink | os.ModePerm +} + +func (lf *Symlink) ModTime() time.Time { + return lf.mtime +} + func (lf *Symlink) Size() (int64, error) { return lf.reader.Size(), nil } diff --git a/multifilereader.go b/multifilereader.go index 3bdc8fc..718cb9c 100644 --- a/multifilereader.go +++ b/multifilereader.go @@ -8,6 +8,8 @@ import ( "net/textproto" "net/url" "path" + "strconv" + "strings" "sync" ) @@ -88,10 +90,7 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { // write the boundary and headers header := make(textproto.MIMEHeader) filename := url.QueryEscape(path.Join(path.Join(mfr.path...), entry.Name())) - dispositionPrefix := "attachment" - if mfr.form { - dispositionPrefix = "form-data; name=\"file\"" - } + mfr.addContentDisposition(header, filename) var contentType string @@ -115,13 +114,8 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { header.Set("abspath", rf.AbsPath()) // attach file size to content-disposition when available // according to https://tools.ietf.org/html/rfc2183 - if stat := rf.Stat(); stat != nil { - dispositionPrefix += fmt.Sprintf("; size=%d", stat.Size()) - } } - header.Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", dispositionPrefix, filename)) - _, err := mfr.mpWriter.CreatePart(header) if err != nil { return 0, err @@ -151,6 +145,36 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { return written, nil } +func (mfr *MultiFileReader) addContentDisposition(header textproto.MIMEHeader, filename string) { + sb := &strings.Builder{} + params := url.Values{} + + if mode := mfr.currentFile.Mode(); mode != 0 { + params.Add("mode", "0"+strconv.FormatUint(uint64(mode), 8)) + } + if mtime := mfr.currentFile.ModTime(); !mtime.IsZero() { + params.Add("mtime", strconv.FormatInt(mtime.Unix(), 10)) + if n := mtime.Nanosecond(); n > 0 { + params.Add("mtime-nsecs", strconv.FormatInt(int64(n), 10)) + } + } + + sb.Grow(120) + if mfr.form { + sb.WriteString("form-data; name=\"file") + if len(params) > 0 { + fmt.Fprintf(sb, "?%s", params.Encode()) + } + sb.WriteString("\"") + } else { + sb.WriteString("attachment") + } + + fmt.Fprintf(sb, "; filename=\"%s\"", url.QueryEscape(filename)) + + header.Set(contentDispositionHeader, sb.String()) +} + // Boundary returns the boundary string to be used to separate files in the multipart data func (mfr *MultiFileReader) Boundary() string { return mfr.mpWriter.Boundary() diff --git a/multipartfile.go b/multipartfile.go index 0d0d6ed..4b61365 100644 --- a/multipartfile.go +++ b/multipartfile.go @@ -5,9 +5,12 @@ import ( "mime" "mime/multipart" "net/url" + "os" "path" + "path/filepath" "strconv" "strings" + "time" ) const ( @@ -22,9 +25,25 @@ const ( contentDispositionHeader = "Content-Disposition" ) +// multiPartFileInfo implements the `fs.FileInfo` interface for a file or +// directory received in a `multipart.part`. +type multiPartFileInfo struct { + name string + mode os.FileMode + mtime time.Time +} + +func (fi *multiPartFileInfo) Name() string { return fi.name } +func (fi *multiPartFileInfo) Mode() os.FileMode { return fi.mode } +func (fi *multiPartFileInfo) ModTime() time.Time { return fi.mtime } +func (fi *multiPartFileInfo) IsDir() bool { return fi.mode.IsDir() } +func (fi *multiPartFileInfo) Sys() interface{} { return nil } +func (fi *multiPartFileInfo) Size() int64 { panic("size for multipart file info is not supported") } + type multipartDirectory struct { path string walker *multipartWalker + stat os.FileInfo // part is the part describing the directory. It's nil when implicit. part *multipart.Part @@ -88,6 +107,8 @@ func (w *multipartWalker) nextFile() (Node, error) { } w.consumePart() + name := fileName(part) + contentType := part.Header.Get(contentTypeHeader) if contentType != "" { var err error @@ -102,6 +123,7 @@ func (w *multipartWalker) nextFile() (Node, error) { return &multipartDirectory{ part: part, path: fileName(part), + stat: fileInfo(name, part), walker: w, }, nil case applicationSymlink: @@ -116,6 +138,7 @@ func (w *multipartWalker) nextFile() (Node, error) { rf := &ReaderFile{ reader: part, abspath: absPath, + stat: fileInfo(name, part), } cdh := part.Header.Get(contentDispositionHeader) _, params, err := mime.ParseMediaType(cdh) @@ -135,6 +158,44 @@ func (w *multipartWalker) nextFile() (Node, error) { } } +// fileInfo constructs an `os.FileInfo` from a `multipart.part` serving +// a file or directory. +func fileInfo(name string, part *multipart.Part) os.FileInfo { + fi := multiPartFileInfo{name: filepath.Base(name)} + formName := part.FormName() + + i := strings.IndexByte(formName, '?') + if i == -1 { + return &fi + } + + params, err := url.ParseQuery(formName[i+1:]) + if err != nil { + return nil + } + + if v := params["mode"]; v != nil { + mode, err := strconv.ParseUint(v[0], 8, 32) + if err == nil { + fi.mode = os.FileMode(mode) + } + } + + var secs, nsecs int64 + if v := params["mtime"]; v != nil { + secs, err = strconv.ParseInt(v[0], 10, 64) + if err != nil { + return &fi + } + } + if v := params["mtime-nsecs"]; v != nil { + nsecs, _ = strconv.ParseInt(v[0], 10, 64) + } + fi.mtime = time.Unix(secs, nsecs) + + return &fi +} + // fileName returns a normalized filename from a part. func fileName(part *multipart.Part) string { v := part.Header.Get("Content-Disposition") @@ -257,6 +318,20 @@ func (f *multipartDirectory) Close() error { return nil } +func (f *multipartDirectory) Mode() os.FileMode { + if f.stat == nil { + return 0 + } + return f.stat.Mode() +} + +func (f *multipartDirectory) ModTime() time.Time { + if f.stat == nil { + return time.Time{} + } + return f.stat.ModTime() +} + func (f *multipartDirectory) Size() (int64, error) { return f.size, nil } diff --git a/readerfile.go b/readerfile.go index 1b54b27..da72c99 100644 --- a/readerfile.go +++ b/readerfile.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "time" ) // ReaderFile is a implementation of File created from an `io.Reader`. @@ -59,6 +60,20 @@ func (f *ReaderFile) Stat() os.FileInfo { return f.stat } +func (f *ReaderFile) Mode() os.FileMode { + if f.stat == nil { + return 0 + } + return f.stat.Mode() +} + +func (f *ReaderFile) ModTime() time.Time { + if f.stat == nil { + return time.Time{} + } + return f.stat.ModTime() +} + func (f *ReaderFile) Size() (int64, error) { if f.stat == nil { if f.fsize >= 0 { diff --git a/serialfile.go b/serialfile.go index 5a7c1aa..c5cec23 100644 --- a/serialfile.go +++ b/serialfile.go @@ -6,6 +6,7 @@ import ( "io/fs" "os" "path/filepath" + "time" ) // serialFile implements Node, and reads from a path on the OS filesystem. @@ -150,9 +151,17 @@ func (f *serialFile) Stat() os.FileInfo { return f.stat } +func (f *serialFile) Mode() os.FileMode { + return f.stat.Mode() +} + +func (f *serialFile) ModTime() time.Time { + return f.stat.ModTime() +} + func (f *serialFile) Size() (int64, error) { if !f.stat.IsDir() { - //something went terribly, terribly wrong + // something went terribly, terribly wrong return 0, errors.New("serialFile is not a directory") } diff --git a/slicedirectory.go b/slicedirectory.go index bbe91c4..e1f9a89 100644 --- a/slicedirectory.go +++ b/slicedirectory.go @@ -2,7 +2,9 @@ package files import ( "errors" + "os" "sort" + "time" ) type fileEntry struct { @@ -55,6 +57,7 @@ func (it *sliceIterator) BreadthFirstTraversal() { // SliceFiles are always directories, and can't be read from or closed. type SliceFile struct { files []DirEntry + stat os.FileInfo } func NewMapDirectory(f map[string]Node) Directory { @@ -70,7 +73,7 @@ func NewMapDirectory(f map[string]Node) Directory { } func NewSliceDirectory(files []DirEntry) Directory { - return &SliceFile{files} + return &SliceFile{files: files} } func (f *SliceFile) Entries() DirIterator { @@ -85,6 +88,20 @@ func (f *SliceFile) Length() int { return len(f.files) } +func (f *SliceFile) Mode() os.FileMode { + if f.stat != nil { + return f.stat.Mode() + } + return 0 +} + +func (f *SliceFile) ModTime() time.Time { + if f.stat != nil { + return f.stat.ModTime() + } + return time.Time{} +} + func (f *SliceFile) Size() (int64, error) { var size int64 diff --git a/webfile.go b/webfile.go index 594b81c..b641b9d 100644 --- a/webfile.go +++ b/webfile.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "time" ) // WebFile is an implementation of File which reads it @@ -16,6 +17,8 @@ type WebFile struct { body io.ReadCloser url *url.URL contentLength int64 + mode os.FileMode + mtime time.Time } // NewWebFile creates a WebFile with the given URL, which @@ -66,6 +69,14 @@ func (wf *WebFile) Seek(offset int64, whence int) (int64, error) { return 0, ErrNotSupported } +func (wf *WebFile) Mode() os.FileMode { + return wf.mode +} + +func (wf *WebFile) ModTime() time.Time { + return wf.mtime +} + func (wf *WebFile) Size() (int64, error) { if err := wf.start(); err != nil { return 0, err