package ftp import ( "bytes" "context" ftpserver "github.com/KirCute/ftpserverlib-pasvportmap" "github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/fs" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/stream" "github.com/alist-org/alist/v3/server/common" "github.com/pkg/errors" "io" "net/http" "os" stdpath "path" "time" ) type FileUploadProxy struct { ftpserver.FileTransfer buffer *os.File path string ctx context.Context trunc bool } func uploadAuth(ctx context.Context, path string) error { user := ctx.Value("user").(*model.User) meta, err := op.GetNearestMeta(stdpath.Dir(path)) if err != nil { if !errors.Is(errors.Cause(err), errs.MetaNotFound) { return err } } if !(common.CanAccess(user, meta, path, ctx.Value("meta_pass").(string)) && ((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) { return errs.PermissionDenied } return nil } func OpenUpload(ctx context.Context, path string, trunc bool) (*FileUploadProxy, error) { err := uploadAuth(ctx, path) if err != nil { return nil, err } tmpFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*") if err != nil { return nil, err } return &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx, trunc: trunc}, nil } func (f *FileUploadProxy) Read(p []byte) (n int, err error) { return 0, errs.NotSupport } func (f *FileUploadProxy) Write(p []byte) (n int, err error) { return f.buffer.Write(p) } func (f *FileUploadProxy) Seek(offset int64, whence int) (int64, error) { return 0, errs.NotSupport } func (f *FileUploadProxy) Close() error { dir, name := stdpath.Split(f.path) size, err := f.buffer.Seek(0, io.SeekCurrent) if err != nil { return err } if _, err := f.buffer.Seek(0, io.SeekStart); err != nil { return err } arr := make([]byte, 512) if _, err := f.buffer.Read(arr); err != nil { return err } contentType := http.DetectContentType(arr) if _, err := f.buffer.Seek(0, io.SeekStart); err != nil { return err } if f.trunc { _ = fs.Remove(f.ctx, f.path) } s := &stream.FileStream{ Obj: &model.Object{ Name: name, Size: size, Modified: time.Now(), }, Mimetype: contentType, WebPutAsTask: true, } s.SetTmpFile(f.buffer) s.Closers.Add(f.buffer) _, err = fs.PutAsTask(f.ctx, dir, s) return err } type FileUploadWithLengthProxy struct { ftpserver.FileTransfer ctx context.Context path string length int64 first512Bytes [512]byte pFirst int pipeWriter io.WriteCloser errChan chan error } func OpenUploadWithLength(ctx context.Context, path string, trunc bool, length int64) (*FileUploadWithLengthProxy, error) { err := uploadAuth(ctx, path) if err != nil { return nil, err } if trunc { _ = fs.Remove(ctx, path) } return &FileUploadWithLengthProxy{ctx: ctx, path: path, length: length}, nil } func (f *FileUploadWithLengthProxy) Read(p []byte) (n int, err error) { return 0, errs.NotSupport } func (f *FileUploadWithLengthProxy) Write(p []byte) (n int, err error) { if f.pipeWriter != nil { select { case e := <-f.errChan: return 0, e default: return f.pipeWriter.Write(p) } } else if len(p) < 512-f.pFirst { copy(f.first512Bytes[f.pFirst:], p) f.pFirst += len(p) return len(p), nil } else { copy(f.first512Bytes[f.pFirst:], p[:512-f.pFirst]) contentType := http.DetectContentType(f.first512Bytes[:]) dir, name := stdpath.Split(f.path) reader, writer := io.Pipe() f.errChan = make(chan error, 1) s := &stream.FileStream{ Obj: &model.Object{ Name: name, Size: f.length, Modified: time.Now(), }, Mimetype: contentType, WebPutAsTask: false, Reader: reader, } go func() { e := fs.PutDirectly(f.ctx, dir, s, true) f.errChan <- e close(f.errChan) }() f.pipeWriter = writer n, err = writer.Write(f.first512Bytes[:]) if err != nil { return n, err } n1, err := writer.Write(p[512-f.pFirst:]) if err != nil { return n1 + 512 - f.pFirst, err } f.pFirst = 512 return len(p), nil } } func (f *FileUploadWithLengthProxy) Seek(offset int64, whence int) (int64, error) { return 0, errs.NotSupport } func (f *FileUploadWithLengthProxy) Close() error { if f.pipeWriter != nil { err := f.pipeWriter.Close() if err != nil { return err } err = <-f.errChan return err } else { data := f.first512Bytes[:f.pFirst] contentType := http.DetectContentType(data) dir, name := stdpath.Split(f.path) s := &stream.FileStream{ Obj: &model.Object{ Name: name, Size: int64(f.pFirst), Modified: time.Now(), }, Mimetype: contentType, WebPutAsTask: false, Reader: bytes.NewReader(data), } return fs.PutDirectly(f.ctx, dir, s, true) } }