···11+# Binaries for programs and plugins
22+*.exe
33+*.exe~
44+*.dll
55+*.so
66+*.dylib
77+88+# Test binary, built with `go test -c`
99+*.test
1010+1111+# Output of the go coverage tool, specifically when used with LiteIDE
1212+*.out
1313+1414+# Dependency directories (remove the comment below to include it)
1515+# vendor/
1616+1717+.env
1818+.DS_Store
1919+tmp/
+21
internal/s3fs/LICENSE
···11+MIT License
22+33+Copyright (c) 2022 Austin Poor
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+5
internal/s3fs/README.md
···11+# s3fs
22+33+_created by Austin Poor_
44+55+An implementation of the [go-billy `Filesystem`](https://pkg.go.dev/github.com/go-git/go-billy/v5#Filesystem) for treating an S3 bucket like a filesystem.
+140
internal/s3fs/basic.go
···11+// basic.go implements the interface billy.Basic
22+33+package main
44+55+import (
66+ "context"
77+ "errors"
88+ "fmt"
99+ "os"
1010+ "path"
1111+1212+ "github.com/aws/aws-sdk-go-v2/service/s3"
1313+ "github.com/go-git/go-billy/v5"
1414+)
1515+1616+const (
1717+ O_RDONLY int = os.O_RDONLY // open the file read-only.
1818+ O_WRONLY int = os.O_WRONLY // open the file write-only.
1919+ O_WRMULTIPART int = 0x4 // open the file for write-only using multipart upload.
2020+2121+ SupportedOFlags = O_RDONLY | O_WRONLY | O_WRMULTIPART // supported open flags for s3fs
2222+)
2323+2424+var (
2525+ ErrOpenFlagNotSupported = errors.New("open flag not supported")
2626+)
2727+2828+// Create implements billy.Basic
2929+// Create creates the named file with mode 0666 (before umask), truncating
3030+// it if it already exists. If successful, methods on the returned File can
3131+// be used for I/O; the associated file descriptor has mode O_RDWR.
3232+func (fs3 *S3FS) Create(filename string) (billy.File, error) {
3333+ return fs3.OpenFile(filename, O_WRONLY, 0666)
3434+}
3535+3636+// Open opens the named file for reading. If successful, methods on the
3737+// returned file can be used for reading; the associated file descriptor has
3838+// mode O_RDONLY.
3939+func (fs3 *S3FS) Open(filename string) (billy.File, error) {
4040+ return fs3.OpenFile(filename, O_RDONLY, 0666)
4141+}
4242+4343+// OpenFile is the generalized open call; most users will use Open or Create
4444+// instead. It opens the named file with specified flag (O_RDONLY etc.) and
4545+// perm, (0666 etc.) if applicable. If successful, methods on the returned
4646+// File can be used for I/O.
4747+func (fs3 *S3FS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
4848+ // Is the supplied flag supported?
4949+ if flag&SupportedOFlags != flag {
5050+ return nil, errors.New("unsupported open flag")
5151+ }
5252+5353+ // Get the file path
5454+ p := path.Join(fs3.root, filename)
5555+5656+ switch flag & SupportedOFlags {
5757+ case O_RDONLY:
5858+ return newS3ReadFile(fs3.client, fs3.bucket, p)
5959+6060+ case O_WRONLY:
6161+ return newS3WriteFile(fs3.client, fs3.bucket, p)
6262+6363+ case O_WRMULTIPART:
6464+ return newS3MultipartUploadFile(fs3.client, fs3.bucket, p)
6565+6666+ default:
6767+ return nil, errors.New("unsupported open flag")
6868+ }
6969+}
7070+7171+// Stat returns a FileInfo describing the named file.
7272+func (fs3 *S3FS) Stat(filename string) (os.FileInfo, error) {
7373+ return nil, errors.New("not implemented")
7474+}
7575+7676+// Rename renames (moves) oldpath to newpath. If newpath already exists and
7777+// is not a directory, Rename replaces it. OS-specific restrictions may
7878+// apply when oldpath and newpath are in different directories.
7979+func (fs3 *S3FS) Rename(oldpath, newpath string) error {
8080+ // TODO: Validate the paths?
8181+8282+ // Create a context
8383+ ctx := context.TODO() // TODO: Get user-supplied context?
8484+8585+ // Format the paths
8686+ src := path.Join(fs3.root, oldpath)
8787+ dst := path.Join(fs3.root, newpath)
8888+8989+ // Send the copy request
9090+ _, err := fs3.client.CopyObject(ctx, &s3.CopyObjectInput{
9191+ Bucket: &fs3.bucket,
9292+ CopySource: &src,
9393+ Key: &dst,
9494+ })
9595+ if err != nil {
9696+ return fmt.Errorf("failed to rename file: %s", err)
9797+ }
9898+9999+ // Delete the old file
100100+ // TODO: Parse the response?
101101+ _, err = fs3.client.DeleteObject(ctx, &s3.DeleteObjectInput{
102102+ Bucket: &fs3.bucket,
103103+ Key: &src,
104104+ })
105105+ if err != nil {
106106+ return fmt.Errorf("failed to remove file: %s", err)
107107+ }
108108+109109+ return nil
110110+}
111111+112112+// Remove removes the named file or directory.
113113+func (fs3 *S3FS) Remove(filename string) error {
114114+ // TODO: Validate the path?
115115+ // ...
116116+117117+ // Create a context
118118+ ctx := context.TODO() // TODO: Get user-supplied context?
119119+120120+ // Format the path
121121+ p := path.Join(fs3.root, filename)
122122+123123+ // Send the request
124124+ // TODO: Parse the response?
125125+ _, err := fs3.client.DeleteObject(ctx, &s3.DeleteObjectInput{
126126+ Bucket: &fs3.bucket,
127127+ Key: &p,
128128+ })
129129+ if err != nil {
130130+ return fmt.Errorf("failed to remove file: %s", err)
131131+ }
132132+ return nil
133133+}
134134+135135+// Join joins any number of path elements into a single path
136136+func (fs3 *S3FS) Join(elem ...string) string {
137137+ j := path.Join(elem...)
138138+ c := path.Clean(j)
139139+ return c
140140+}
+29
internal/s3fs/chroot.go
···11+// chroot.go implements the interface billy.Chroot
22+33+package main
44+55+import "github.com/go-git/go-billy/v5"
66+77+// Chroot returns a new filesystem from the same type where the new root is
88+// the given path. Files outside of the designated directory tree cannot be
99+// accessed.
1010+func (fs3 *S3FS) Chroot(path string) (billy.Filesystem, error) {
1111+ // TODO: Check that path is a valid subdirectory of the current root
1212+ // ...
1313+1414+ // Calculate the new root
1515+ p := fs3.Join(fs3.root, path)
1616+1717+ // Create the new S3FS with the new root directory
1818+ nfs := &S3FS{
1919+ client: fs3.client,
2020+ bucket: fs3.bucket,
2121+ root: p,
2222+ }
2323+ return nfs, nil
2424+}
2525+2626+// Root returns the root path of the filesystem.
2727+func (fs3 *S3FS) Root() string {
2828+ return fs3.root
2929+}
+75
internal/s3fs/dir.go
···11+// dir.go implements the interface billy.Dir
22+33+package main
44+55+import (
66+ "context"
77+ "errors"
88+ "os"
99+1010+ "github.com/aws/aws-sdk-go-v2/aws"
1111+ "github.com/aws/aws-sdk-go-v2/service/s3"
1212+)
1313+1414+// ReadDir reads the directory named by dirname and returns a list of
1515+// directory entries sorted by filename.
1616+func (fs3 *S3FS) ReadDir(path string) ([]os.FileInfo, error) {
1717+ // p := fs3.cleanPath(fs3.root, path)
1818+ // if p != "" {
1919+ // p += "/"
2020+ // }
2121+ // fmt.Println("ReadDir:", p)
2222+ p := path
2323+2424+ // Create a context with a timeout
2525+ ctx := context.TODO() // TODO: Get user context?
2626+2727+ var ct *string
2828+ var dirs []os.FileInfo
2929+ var files []os.FileInfo
3030+ for {
3131+ res, err := fs3.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
3232+ Bucket: &fs3.bucket,
3333+ Prefix: &p,
3434+ ContinuationToken: ct,
3535+ Delimiter: &fs3.separator,
3636+ })
3737+ if err != nil {
3838+ return nil, err
3939+ }
4040+4141+ // Add the directories to the list
4242+ for _, d := range res.CommonPrefixes {
4343+ dirs = append(dirs, newDirInfo(*d.Prefix))
4444+ }
4545+4646+ // Add the files to the list
4747+ for _, f := range res.Contents {
4848+ files = append(files, newFileInfo(
4949+ aws.ToString(f.Key),
5050+ *f.Size,
5151+ aws.ToTime(f.LastModified),
5252+ ))
5353+ }
5454+5555+ // Set the last key
5656+ ct = res.NextContinuationToken
5757+5858+ // If there are no more keys, break
5959+ if !*res.IsTruncated {
6060+ break
6161+ }
6262+ }
6363+6464+ // Join the directories and files & return
6565+ res := append(dirs, files...)
6666+ return res, nil
6767+}
6868+6969+// MkdirAll creates a directory named path, along with any necessary
7070+// parents, and returns nil, or else returns an error. The permission bits
7171+// perm are used for all directories that MkdirAll creates. If path is/
7272+// already a directory, MkdirAll does nothing and returns nil.
7373+func (fs3 *S3FS) MkdirAll(filename string, perm os.FileMode) error {
7474+ return errors.New("not implemented")
7575+}
+355
internal/s3fs/file.go
···11+package main
22+33+import (
44+ "bytes"
55+ "context"
66+ "errors"
77+ "fmt"
88+ "io/fs"
99+ "io/ioutil"
1010+ "os"
1111+1212+ "github.com/aws/aws-sdk-go-v2/service/s3"
1313+ "go.uber.org/atomic"
1414+)
1515+1616+const (
1717+ ModeMultipartUpload os.FileMode = fs.ModePerm + 1 // Custom os.FileMode for S3 multipart upload
1818+)
1919+2020+var (
2121+ ErrLockNotSupported = errors.New("lock not supported by s3")
2222+ ErrTruncateNotSupported = errors.New("truncate not supported by s3")
2323+ ErrFileClosed = errors.New("file is closed")
2424+ ErrCantWriteToReadOnly = errors.New("can't write to read-only file")
2525+ ErrCantReadFromWriteOnly = errors.New("can't read from write-only file")
2626+)
2727+2828+// s3ReadFile implements billy.File for S3, and represents a file opened in read mode.
2929+//
3030+// Upon creation, the file is loaded from S3.
3131+type s3ReadFile struct {
3232+ client *s3.Client // s3 skd client
3333+ bucket string // S3 bucket name
3434+ key string // File object's key in S3
3535+ closed bool // Is the file closed?
3636+ reader *bytes.Reader // Buffer for file contents
3737+}
3838+3939+// newS3ReadFile creates a new s3ReadFile.
4040+func newS3ReadFile(client *s3.Client, bucket, key string) (*s3ReadFile, error) {
4141+ // TODO: Check if the file exists
4242+ // ...
4343+4444+ // Create the context
4545+ ctx := context.TODO() // TODO: How can user-supplied contexts be supported?
4646+4747+ // Run the GetObject operation
4848+ res, err := client.GetObject(ctx, &s3.GetObjectInput{
4949+ Bucket: &bucket,
5050+ Key: &key,
5151+ })
5252+ if err != nil {
5353+ return nil, fmt.Errorf("unable to perform GetObject operation: %w", err)
5454+ }
5555+5656+ // Read the file contents and store in a bytes reader
5757+ buf, err := ioutil.ReadAll(res.Body)
5858+ if err != nil {
5959+ return nil, fmt.Errorf("unable to read file body: %w", err)
6060+ }
6161+ reader := bytes.NewReader(buf)
6262+6363+ // Return the file
6464+ return &s3ReadFile{
6565+ client: client,
6666+ bucket: bucket,
6767+ key: key,
6868+ reader: reader,
6969+ }, nil
7070+}
7171+7272+// Name returns the name of the file as presented to Open.
7373+func (f *s3ReadFile) Name() string {
7474+ return f.key
7575+}
7676+7777+// Write implements os.Writer for billy.File
7878+func (f *s3ReadFile) Write(p []byte) (n int, err error) {
7979+ return 0, ErrCantWriteToReadOnly
8080+}
8181+8282+// Read implements os.Reader for billy.File
8383+func (f *s3ReadFile) Read(p []byte) (n int, err error) {
8484+ return f.reader.Read(p)
8585+}
8686+8787+// ReadAt implements io.ReaderAt for billy.File
8888+func (f *s3ReadFile) ReadAt(p []byte, off int64) (n int, err error) {
8989+ return f.reader.ReadAt(p, off)
9090+}
9191+9292+// Seek implements io.Seeker for billy.File
9393+func (f *s3ReadFile) Seek(offset int64, whence int) (int64, error) {
9494+ return f.reader.Seek(offset, whence)
9595+}
9696+9797+// Close implements io.Closer for billy.File
9898+func (f *s3ReadFile) Close() error {
9999+ // Was the file already closed?
100100+ if f.closed {
101101+ return ErrFileClosed
102102+ }
103103+104104+ // Close the underlying file
105105+ f.reader = nil
106106+107107+ // Mark the file as closed
108108+ f.closed = true
109109+110110+ return nil
111111+}
112112+113113+// Lock locks the file like e.g. flock. It protects against access from
114114+// other processes.
115115+func (f *s3ReadFile) Lock() error {
116116+ return ErrLockNotSupported
117117+}
118118+119119+// Unlock unlocks the file.
120120+func (f *s3ReadFile) Unlock() error {
121121+ return ErrLockNotSupported
122122+}
123123+124124+// Truncate the file.
125125+func (f *s3ReadFile) Truncate(size int64) error {
126126+ return ErrTruncateNotSupported
127127+}
128128+129129+// s3WriteFile stores a file opened in write mode and implements billy.File
130130+//
131131+// Upon creation, a buffer is created to store the file contents. Upon close,
132132+// the file is uploaded to S3.
133133+type s3WriteFile struct {
134134+ client *s3.Client // s3 skd client
135135+ bucket string // S3 bucket name
136136+ key string // File object's key in S3
137137+ closed bool // Is the file closed?
138138+ buf *bytes.Buffer // Buffer for storing the file before it's uploaded
139139+}
140140+141141+// newS3WriteFile creates a new s3ReadFile.
142142+func newS3WriteFile(client *s3.Client, bucket, key string) (*s3WriteFile, error) {
143143+ // TODO: Validate the key
144144+ // ...
145145+146146+ return &s3WriteFile{
147147+ client: client,
148148+ bucket: bucket,
149149+ key: key,
150150+ buf: bytes.NewBuffer(nil),
151151+ }, nil
152152+}
153153+154154+// Name returns the name of the file as presented to Open.
155155+func (f *s3WriteFile) Name() string {
156156+ return f.key
157157+}
158158+159159+// Write implements os.Writer for billy.File
160160+func (f *s3WriteFile) Write(p []byte) (n int, err error) {
161161+ return 0, nil
162162+}
163163+164164+// Read implements os.Reader for billy.File
165165+func (f *s3WriteFile) Read(p []byte) (n int, err error) {
166166+ return 0, ErrCantReadFromWriteOnly
167167+}
168168+169169+// ReadAt implements io.ReaderAt for billy.File
170170+func (f *s3WriteFile) ReadAt(p []byte, off int64) (n int, err error) {
171171+ return 0, ErrCantReadFromWriteOnly
172172+}
173173+174174+// Seek implements io.Seeker for billy.File
175175+func (f *s3WriteFile) Seek(offset int64, whence int) (int64, error) {
176176+ return 0, errors.New("not implemented")
177177+}
178178+179179+// Close implements io.Closer for billy.File
180180+func (f *s3WriteFile) Close() error {
181181+ if f.closed {
182182+ return ErrFileClosed
183183+ }
184184+185185+ // Set to closed
186186+ f.closed = true
187187+188188+ // Extract the body from the buffer
189189+ body := bytes.NewReader(f.buf.Bytes())
190190+191191+ // Create the context
192192+ ctx := context.TODO() // TODO: How can user-supplied contexts be supported?
193193+194194+ // Run the GetObject operation
195195+ // TODO: Currently `res` is not used. Should it be?
196196+ _, err := f.client.PutObject(ctx, &s3.PutObjectInput{
197197+ Bucket: &f.bucket,
198198+ Key: &f.key,
199199+ Body: body,
200200+ })
201201+ if err != nil {
202202+ return fmt.Errorf("unable to perform GetObject operation: %w", err)
203203+ }
204204+205205+ return nil
206206+}
207207+208208+// Lock locks the file like e.g. flock. It protects against access from
209209+// other processes.
210210+func (f *s3WriteFile) Lock() error {
211211+ return ErrLockNotSupported
212212+}
213213+214214+// Unlock unlocks the file.
215215+func (f *s3WriteFile) Unlock() error {
216216+ return ErrLockNotSupported
217217+}
218218+219219+// Truncate the file.
220220+func (f *s3WriteFile) Truncate(size int64) error {
221221+ return ErrTruncateNotSupported
222222+}
223223+224224+// s3MultipartUploadFile implements billy.File
225225+type s3MultipartUploadFile struct {
226226+ client *s3.Client // s3 skd client
227227+ bucket string // S3 bucket name
228228+ key string // File object's key in S3
229229+ closed bool // Is the file closed?
230230+ uploadID string // S3 multipart upload ID
231231+ uploadN *atomic.Int32 // Counter tracking the number of uploads
232232+}
233233+234234+// newS3MultipartUploadFile creates a new s3ReadFile.
235235+func newS3MultipartUploadFile(client *s3.Client, bucket, key string) (*s3MultipartUploadFile, error) {
236236+ // TODO: Check if the file exists
237237+ // ...
238238+239239+ // Create the context
240240+ ctx := context.TODO() // TODO: How can user-supplied contexts be supported?
241241+242242+ // Run the GetObject operation
243243+ res, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
244244+ Bucket: &bucket,
245245+ Key: &key,
246246+ })
247247+ if err != nil {
248248+ return nil, fmt.Errorf("unable to create multipart upload: %w", err)
249249+ }
250250+251251+ // Return the file
252252+ return &s3MultipartUploadFile{
253253+ client: client,
254254+ bucket: bucket,
255255+ key: key,
256256+ uploadID: *res.UploadId,
257257+ uploadN: atomic.NewInt32(1),
258258+ }, nil
259259+}
260260+261261+// Name returns the name of the file as presented to Open.
262262+func (f *s3MultipartUploadFile) Name() string {
263263+ return f.key
264264+}
265265+266266+// Write implements os.Writer for billy.File
267267+func (f *s3MultipartUploadFile) Write(p []byte) (n int, err error) {
268268+ // Get the size of the data being written
269269+ n = len(p)
270270+271271+ // Create a context for the operation
272272+ ctx := context.TODO() // TODO: How can user-supplied contexts be supported?
273273+274274+ // Create a reader for the data
275275+ r := bytes.NewReader(p)
276276+277277+ // Get the part number
278278+ pn := f.uploadN.Load()
279279+280280+ // Run the UploadPart operation
281281+ _, err = f.client.UploadPart(ctx, &s3.UploadPartInput{
282282+ Bucket: &f.bucket,
283283+ Key: &f.key,
284284+ UploadId: &f.uploadID,
285285+ PartNumber: new(pn),
286286+ Body: r,
287287+ })
288288+ if err != nil {
289289+ return 0, fmt.Errorf("unable to upload part %d: %w", pn, err)
290290+ }
291291+292292+ // Increment the part number
293293+ f.uploadN.Add(1)
294294+295295+ // Return the number of bytes written
296296+ return n, nil
297297+}
298298+299299+// Read implements os.Reader for billy.File
300300+func (f *s3MultipartUploadFile) Read(p []byte) (n int, err error) {
301301+ return 0, ErrCantReadFromWriteOnly
302302+}
303303+304304+// ReadAt implements io.ReaderAt for billy.File
305305+func (f *s3MultipartUploadFile) ReadAt(p []byte, off int64) (n int, err error) {
306306+ return 0, ErrCantReadFromWriteOnly
307307+}
308308+309309+// Seek implements io.Seeker for billy.File
310310+func (f *s3MultipartUploadFile) Seek(offset int64, whence int) (int64, error) {
311311+ return 0, errors.New("seek not implemented")
312312+}
313313+314314+// Close implements io.Closer for billy.File
315315+func (f *s3MultipartUploadFile) Close() error {
316316+ // Check if the file has been closed
317317+ if f.closed {
318318+ return ErrFileClosed
319319+ }
320320+321321+ // Set to closed
322322+ f.closed = true
323323+324324+ // Create the context
325325+ ctx := context.TODO() // TODO: How can user-supplied contexts be supported?
326326+327327+ // Complete the multipart upload
328328+ // TODO: Currently `res` is not used. Should it be?
329329+ _, err := f.client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
330330+ Bucket: &f.bucket,
331331+ Key: &f.key,
332332+ UploadId: &f.uploadID,
333333+ })
334334+ if err != nil {
335335+ return fmt.Errorf("unable to complete multipart upload: %w", err)
336336+ }
337337+338338+ return nil
339339+}
340340+341341+// Lock locks the file like e.g. flock. It protects against access from
342342+// other processes.
343343+func (f *s3MultipartUploadFile) Lock() error {
344344+ return ErrLockNotSupported
345345+}
346346+347347+// Unlock unlocks the file.
348348+func (f *s3MultipartUploadFile) Unlock() error {
349349+ return ErrLockNotSupported
350350+}
351351+352352+// Truncate the file.
353353+func (f *s3MultipartUploadFile) Truncate(size int64) error {
354354+ return ErrTruncateNotSupported
355355+}
···11+// symlink.go implements the interface billy.Symlink
22+33+package main
44+55+import (
66+ "errors"
77+ "os"
88+)
99+1010+var (
1111+ ErrSymLinkNotSupported = errors.New("symlink not supported by s3")
1212+)
1313+1414+// Lstat returns a FileInfo describing the named file. If the file is a
1515+// symbolic link, the returned FileInfo describes the symbolic link. Lstat
1616+// makes no attempt to follow the link.
1717+//
1818+// NOTE: Lstat is not supported by s3. It always returns an error.
1919+// (This may be revised in the future.)
2020+func (fs3 *S3FS) Lstat(filename string) (os.FileInfo, error) {
2121+ return nil, ErrSymLinkNotSupported
2222+}
2323+2424+// Symlink creates a symbolic-link from link to target. target may be an
2525+// absolute or relative path, and need not refer to an existing node.
2626+// Parent directories of link are created as necessary.
2727+//
2828+// NOTE: Symlink is not supported by s3. It always returns an error.
2929+func (fs3 *S3FS) Symlink(target, link string) error {
3030+ return ErrSymLinkNotSupported
3131+}
3232+3333+// Readlink returns the target path of link.
3434+//
3535+// NOTE: Readlink is not supported by s3. It always returns an error.
3636+// (This may be revised in the future.)
3737+func (fs3 *S3FS) Readlink(link string) (string, error) {
3838+ return "", ErrSymLinkNotSupported
3939+}
+17
internal/s3fs/tempfile.go
···11+// tempfile.go implements the interface billy.TempFile
22+33+package main
44+55+import "github.com/go-git/go-billy/v5"
66+77+// TempFile creates a new temporary file in the directory dir with a name
88+// beginning with prefix, opens the file for reading and writing, and
99+// returns the resulting *os.File. If dir is the empty string, TempFile
1010+// uses the default directory for temporary files (see os.TempDir).
1111+// Multiple programs calling TempFile simultaneously will not choose the
1212+// same file. The caller can use f.Name() to find the pathname of the file.
1313+// It is the caller's responsibility to remove the file when no longer
1414+// needed.
1515+func (fs3 *S3FS) TempFile(dir, prefix string) (billy.File, error) {
1616+ return nil, nil
1717+}