123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- // Copyright (C) 2019-2023 Nicola Murino
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published
- // by the Free Software Foundation, version 3.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package webdavd
- import (
- "context"
- "encoding/xml"
- "errors"
- "io"
- "mime"
- "net/http"
- "os"
- "path"
- "sync/atomic"
- "time"
- "github.com/drakkan/webdav"
- "github.com/eikenb/pipeat"
- "github.com/drakkan/sftpgo/v2/internal/common"
- "github.com/drakkan/sftpgo/v2/internal/dataprovider"
- "github.com/drakkan/sftpgo/v2/internal/logger"
- "github.com/drakkan/sftpgo/v2/internal/util"
- "github.com/drakkan/sftpgo/v2/internal/vfs"
- )
- var (
- errTransferAborted = errors.New("transfer aborted")
- lastModifiedProps = []string{"Win32LastModifiedTime", "getlastmodified"}
- )
- type webDavFile struct {
- *common.BaseTransfer
- writer io.WriteCloser
- reader io.ReadCloser
- info os.FileInfo
- startOffset int64
- isFinished bool
- readTryed atomic.Bool
- }
- func newWebDavFile(baseTransfer *common.BaseTransfer, pipeWriter *vfs.PipeWriter, pipeReader *pipeat.PipeReaderAt) *webDavFile {
- var writer io.WriteCloser
- var reader io.ReadCloser
- if baseTransfer.File != nil {
- writer = baseTransfer.File
- reader = baseTransfer.File
- } else if pipeWriter != nil {
- writer = pipeWriter
- } else if pipeReader != nil {
- reader = pipeReader
- }
- f := &webDavFile{
- BaseTransfer: baseTransfer,
- writer: writer,
- reader: reader,
- isFinished: false,
- startOffset: 0,
- info: nil,
- }
- f.readTryed.Store(false)
- return f
- }
- type webDavFileInfo struct {
- os.FileInfo
- Fs vfs.Fs
- virtualPath string
- fsPath string
- }
- // ContentType implements webdav.ContentTyper interface
- func (fi *webDavFileInfo) ContentType(_ context.Context) (string, error) {
- extension := path.Ext(fi.virtualPath)
- if ctype, ok := customMimeTypeMapping[extension]; ok {
- return ctype, nil
- }
- if extension == "" || extension == ".dat" {
- return "application/octet-stream", nil
- }
- contentType := mime.TypeByExtension(extension)
- if contentType != "" {
- return contentType, nil
- }
- contentType = mimeTypeCache.getMimeFromCache(extension)
- if contentType != "" {
- return contentType, nil
- }
- contentType, err := fi.Fs.GetMimeType(fi.fsPath)
- if contentType != "" {
- mimeTypeCache.addMimeToCache(extension, contentType)
- return contentType, err
- }
- return "", webdav.ErrNotImplemented
- }
- // Readdir reads directory entries from the handle
- func (f *webDavFile) Readdir(_ int) ([]os.FileInfo, error) {
- if !f.Connection.User.HasPerm(dataprovider.PermListItems, f.GetVirtualPath()) {
- return nil, f.Connection.GetPermissionDeniedError()
- }
- entries, err := f.Connection.ListDir(f.GetVirtualPath())
- if err != nil {
- return nil, err
- }
- for idx, info := range entries {
- entries[idx] = &webDavFileInfo{
- FileInfo: info,
- Fs: f.Fs,
- virtualPath: path.Join(f.GetVirtualPath(), info.Name()),
- fsPath: f.Fs.Join(f.GetFsPath(), info.Name()),
- }
- }
- return entries, nil
- }
- // Stat the handle
- func (f *webDavFile) Stat() (os.FileInfo, error) {
- if f.GetType() == common.TransferDownload && !f.Connection.User.HasPerm(dataprovider.PermListItems, path.Dir(f.GetVirtualPath())) {
- return nil, f.Connection.GetPermissionDeniedError()
- }
- f.Lock()
- errUpload := f.ErrTransfer
- f.Unlock()
- if f.GetType() == common.TransferUpload && errUpload == nil {
- info := &webDavFileInfo{
- FileInfo: vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Now(), false),
- Fs: f.Fs,
- virtualPath: f.GetVirtualPath(),
- fsPath: f.GetFsPath(),
- }
- return info, nil
- }
- info, err := f.Fs.Stat(f.GetFsPath())
- if err != nil {
- return nil, err
- }
- if vfs.IsCryptOsFs(f.Fs) {
- info = f.Fs.(*vfs.CryptFs).ConvertFileInfo(info)
- }
- fi := &webDavFileInfo{
- FileInfo: info,
- Fs: f.Fs,
- virtualPath: f.GetVirtualPath(),
- fsPath: f.GetFsPath(),
- }
- return fi, nil
- }
- func (f *webDavFile) checkFirstRead() error {
- if !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {
- return f.Connection.GetPermissionDeniedError()
- }
- transferQuota := f.BaseTransfer.GetTransferQuota()
- if !transferQuota.HasDownloadSpace() {
- f.Connection.Log(logger.LevelInfo, "denying file read due to quota limits")
- return f.Connection.GetReadQuotaExceededError()
- }
- if ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {
- f.Connection.Log(logger.LevelWarn, "reading file %q is not allowed", f.GetVirtualPath())
- return f.Connection.GetErrorForDeniedFile(policy)
- }
- _, err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
- if err != nil {
- f.Connection.Log(logger.LevelDebug, "download for file %q denied by pre action: %v", f.GetVirtualPath(), err)
- return f.Connection.GetPermissionDeniedError()
- }
- f.readTryed.Store(true)
- return nil
- }
- // Read reads the contents to downloads.
- func (f *webDavFile) Read(p []byte) (n int, err error) {
- if f.AbortTransfer.Load() {
- return 0, errTransferAborted
- }
- if !f.readTryed.Load() {
- if err := f.checkFirstRead(); err != nil {
- return 0, err
- }
- }
- f.Connection.UpdateLastActivity()
- // the file is read sequentially we don't need to check for concurrent reads and so
- // lock the transfer while opening the remote file
- if f.reader == nil {
- if f.GetType() != common.TransferDownload {
- f.TransferError(common.ErrOpUnsupported)
- return 0, common.ErrOpUnsupported
- }
- file, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)
- f.Lock()
- if e == nil {
- if file != nil {
- f.File = file
- f.writer = f.File
- f.reader = f.File
- } else if r != nil {
- f.reader = r
- }
- f.BaseTransfer.SetCancelFn(cancelFn)
- }
- f.ErrTransfer = e
- f.startOffset = 0
- f.Unlock()
- if e != nil {
- return 0, e
- }
- }
- n, err = f.reader.Read(p)
- f.BytesSent.Add(int64(n))
- if err == nil {
- err = f.CheckRead()
- }
- if err != nil && err != io.EOF {
- f.TransferError(err)
- return
- }
- f.HandleThrottle()
- return
- }
- // Write writes the uploaded contents.
- func (f *webDavFile) Write(p []byte) (n int, err error) {
- if f.AbortTransfer.Load() {
- return 0, errTransferAborted
- }
- f.Connection.UpdateLastActivity()
- n, err = f.writer.Write(p)
- f.BytesReceived.Add(int64(n))
- if err == nil {
- err = f.CheckWrite()
- }
- if err != nil {
- f.TransferError(err)
- return
- }
- f.HandleThrottle()
- return
- }
- func (f *webDavFile) updateStatInfo() error {
- if f.info != nil {
- return nil
- }
- info, err := f.Fs.Stat(f.GetFsPath())
- if err != nil {
- return err
- }
- if vfs.IsCryptOsFs(f.Fs) {
- info = f.Fs.(*vfs.CryptFs).ConvertFileInfo(info)
- }
- f.info = info
- return nil
- }
- func (f *webDavFile) updateTransferQuotaOnSeek() {
- transferQuota := f.GetTransferQuota()
- if transferQuota.HasSizeLimits() {
- go func(ulSize, dlSize int64, user dataprovider.User) {
- dataprovider.UpdateUserTransferQuota(&user, ulSize, dlSize, false) //nolint:errcheck
- }(f.BytesReceived.Load(), f.BytesSent.Load(), f.Connection.User)
- }
- }
- func (f *webDavFile) checkFile() error {
- if f.File == nil && vfs.IsLocalOrUnbufferedSFTPFs(f.Fs) {
- file, _, _, err := f.Fs.Open(f.GetFsPath(), 0)
- if err != nil {
- f.Connection.Log(logger.LevelWarn, "could not open file %q for seeking: %v",
- f.GetFsPath(), err)
- f.TransferError(err)
- return err
- }
- f.File = file
- f.reader = file
- f.writer = file
- }
- return nil
- }
- func (f *webDavFile) seekFile(offset int64, whence int) (int64, error) {
- ret, err := f.File.Seek(offset, whence)
- if err != nil {
- f.TransferError(err)
- }
- return ret, err
- }
- // Seek sets the offset for the next Read or Write on the writer to offset,
- // interpreted according to whence: 0 means relative to the origin of the file,
- // 1 means relative to the current offset, and 2 means relative to the end.
- // It returns the new offset and an error, if any.
- func (f *webDavFile) Seek(offset int64, whence int) (int64, error) {
- f.Connection.UpdateLastActivity()
- if err := f.checkFile(); err != nil {
- return 0, err
- }
- if f.File != nil {
- return f.seekFile(offset, whence)
- }
- if f.GetType() == common.TransferDownload {
- readOffset := f.startOffset + f.BytesSent.Load()
- if offset == 0 && readOffset == 0 {
- if whence == io.SeekStart {
- return 0, nil
- } else if whence == io.SeekEnd {
- if err := f.updateStatInfo(); err != nil {
- return 0, err
- }
- return f.info.Size(), nil
- }
- }
- // close the reader and create a new one at startByte
- if f.reader != nil {
- f.reader.Close() //nolint:errcheck
- f.reader = nil
- }
- startByte := int64(0)
- f.BytesReceived.Store(0)
- f.BytesSent.Store(0)
- f.updateTransferQuotaOnSeek()
- switch whence {
- case io.SeekStart:
- startByte = offset
- case io.SeekCurrent:
- startByte = readOffset + offset
- case io.SeekEnd:
- if err := f.updateStatInfo(); err != nil {
- f.TransferError(err)
- return 0, err
- }
- startByte = f.info.Size() - offset
- }
- _, r, cancelFn, err := f.Fs.Open(f.GetFsPath(), startByte)
- f.Lock()
- if err == nil {
- f.startOffset = startByte
- f.reader = r
- }
- f.ErrTransfer = err
- f.BaseTransfer.SetCancelFn(cancelFn)
- f.Unlock()
- return startByte, err
- }
- return 0, common.ErrOpUnsupported
- }
- // Close closes the open directory or the current transfer
- func (f *webDavFile) Close() error {
- if err := f.setFinished(); err != nil {
- return err
- }
- err := f.closeIO()
- if f.isTransfer() {
- errBaseClose := f.BaseTransfer.Close()
- if errBaseClose != nil {
- err = errBaseClose
- }
- } else {
- f.Connection.RemoveTransfer(f.BaseTransfer)
- }
- return f.Connection.GetFsError(f.Fs, err)
- }
- func (f *webDavFile) closeIO() error {
- var err error
- if f.File != nil {
- err = f.File.Close()
- } else if f.writer != nil {
- err = f.writer.Close()
- f.Lock()
- // we set ErrTransfer here so quota is not updated, in this case the uploads are atomic
- if err != nil && f.ErrTransfer == nil {
- f.ErrTransfer = err
- }
- f.Unlock()
- } else if f.reader != nil {
- err = f.reader.Close()
- }
- return err
- }
- func (f *webDavFile) setFinished() error {
- f.Lock()
- defer f.Unlock()
- if f.isFinished {
- return common.ErrTransferClosed
- }
- f.isFinished = true
- return nil
- }
- func (f *webDavFile) isTransfer() bool {
- if f.GetType() == common.TransferDownload {
- return f.readTryed.Load()
- }
- return true
- }
- // DeadProps returns a copy of the dead properties held.
- // We always return nil for now, we only support the last modification time
- // and it is already included in "live" properties
- func (f *webDavFile) DeadProps() (map[xml.Name]webdav.Property, error) {
- return nil, nil
- }
- // Patch patches the dead properties held.
- // In our minimal implementation we just support Win32LastModifiedTime and
- // getlastmodified to set the the modification time.
- // We ignore any other property and just return an OK response if the patch sets
- // the modification time, otherwise a Forbidden response
- func (f *webDavFile) Patch(patches []webdav.Proppatch) ([]webdav.Propstat, error) {
- resp := make([]webdav.Propstat, 0, len(patches))
- hasError := false
- for _, patch := range patches {
- status := http.StatusForbidden
- pstat := webdav.Propstat{}
- for _, p := range patch.Props {
- if status == http.StatusForbidden && !hasError {
- if !patch.Remove && util.Contains(lastModifiedProps, p.XMLName.Local) {
- parsed, err := http.ParseTime(string(p.InnerXML))
- if err != nil {
- f.Connection.Log(logger.LevelWarn, "unsupported last modification time: %q, err: %v",
- string(p.InnerXML), err)
- hasError = true
- continue
- }
- attrs := &common.StatAttributes{
- Flags: common.StatAttrTimes,
- Atime: parsed,
- Mtime: parsed,
- }
- if err := f.Connection.SetStat(f.GetVirtualPath(), attrs); err != nil {
- f.Connection.Log(logger.LevelWarn, "unable to set modification time for %q, err :%v",
- f.GetVirtualPath(), err)
- hasError = true
- continue
- }
- status = http.StatusOK
- }
- }
- pstat.Props = append(pstat.Props, webdav.Property{XMLName: p.XMLName})
- }
- pstat.Status = status
- resp = append(resp, pstat)
- }
- return resp, nil
- }
|