Skip to content

Commit

Permalink
Merge pull request #4 from bittorrent/feat/unixfs-mtime
Browse files Browse the repository at this point in the history
Feat/unixfs mtime
  • Loading branch information
mengcody authored Dec 6, 2024
2 parents 0b3581a + 172ed5a commit 1343cef
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 11 deletions.
9 changes: 9 additions & 0 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"io"
"os"
"time"
)

var (
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions linkfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
42 changes: 33 additions & 9 deletions multifilereader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/textproto"
"net/url"
"path"
"strconv"
"strings"
"sync"
)

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
77 changes: 77 additions & 0 deletions multipartfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"mime"
"mime/multipart"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)

const (
Expand All @@ -22,9 +25,26 @@ 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
size int64
}

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 { return fi.size }

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
Expand Down Expand Up @@ -88,6 +108,8 @@ func (w *multipartWalker) nextFile() (Node, error) {
}
w.consumePart()

name := fileName(part)

contentType := part.Header.Get(contentTypeHeader)
if contentType != "" {
var err error
Expand All @@ -102,6 +124,7 @@ func (w *multipartWalker) nextFile() (Node, error) {
return &multipartDirectory{
part: part,
path: fileName(part),
stat: fileInfo(name, part),
walker: w,
}, nil
case applicationSymlink:
Expand All @@ -116,6 +139,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)
Expand All @@ -135,6 +159,45 @@ 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")
Expand Down Expand Up @@ -257,6 +320,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
}
Expand Down
15 changes: 15 additions & 0 deletions readerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"time"
)

// ReaderFile is a implementation of File created from an `io.Reader`.
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 10 additions & 1 deletion serialfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/fs"
"os"
"path/filepath"
"time"
)

// serialFile implements Node, and reads from a path on the OS filesystem.
Expand Down Expand Up @@ -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")
}

Expand Down
19 changes: 18 additions & 1 deletion slicedirectory.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package files

import (
"errors"
"os"
"sort"
"time"
)

type fileEntry struct {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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

Expand Down
Loading

0 comments on commit 1343cef

Please sign in to comment.