mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 01:58:11 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			339 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package model
 | |
| 
 | |
| /*
 | |
|  * Implementation of a webdav.FileSystem: https://godoc.org/golang.org/x/net/webdav#FileSystem that is used
 | |
|  * to generate our webdav server.
 | |
|  * A lot of memoization is happening so that we don't DDOS the underlying storage which was important
 | |
|  * considering most webdav client within OS are extremely greedy in HTTP request
 | |
|  */
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	. "github.com/mickael-kerjean/filestash/server/common"
 | |
| 	"github.com/mickael-kerjean/net/webdav"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| const DAVCachePath = "data/cache/webdav/"
 | |
| 
 | |
| var (
 | |
| 	cachePath   string
 | |
| 	webdavCache AppCache
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	cachePath = GetAbsolutePath(DAVCachePath) + "/"
 | |
| 	os.RemoveAll(cachePath)
 | |
| 	os.MkdirAll(cachePath, os.ModePerm)
 | |
| 
 | |
| 	webdavCache = NewQuickCache(20, 10)
 | |
| 	webdavCache.OnEvict(func(filename string, _ interface{}) {
 | |
| 		os.Remove(filename)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type WebdavFs struct {
 | |
| 	req        *http.Request
 | |
| 	backend    IBackend
 | |
| 	path       string
 | |
| 	id         string
 | |
| 	chroot     string
 | |
| 	webdavFile *WebdavFile
 | |
| }
 | |
| 
 | |
| func NewWebdavFs(b IBackend, primaryKey string, chroot string, req *http.Request) *WebdavFs {
 | |
| 	return &WebdavFs{
 | |
| 		backend: b,
 | |
| 		id:      primaryKey,
 | |
| 		chroot:  chroot,
 | |
| 		req:     req,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (this WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
 | |
| 	if name = this.fullpath(name); name == "" {
 | |
| 		return os.ErrNotExist
 | |
| 	}
 | |
| 	return this.backend.Mkdir(name)
 | |
| }
 | |
| 
 | |
| func (this *WebdavFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
 | |
| 	cachePath := fmt.Sprintf("%stmp_%s", cachePath, Hash(this.id+name, 20))
 | |
| 	fwriteFile := func() *os.File {
 | |
| 		if this.req.Method == "PUT" {
 | |
| 			f, err := os.OpenFile(cachePath+"_writer", os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm)
 | |
| 			if err != nil {
 | |
| 				return nil
 | |
| 			}
 | |
| 			return f
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| 	if this.webdavFile != nil {
 | |
| 		this.webdavFile.fwrite = fwriteFile()
 | |
| 		return this.webdavFile, nil
 | |
| 	}
 | |
| 	if name = this.fullpath(name); name == "" {
 | |
| 		return nil, os.ErrNotExist
 | |
| 	}
 | |
| 	this.webdavFile = &WebdavFile{
 | |
| 		path:    name,
 | |
| 		backend: this.backend,
 | |
| 		cache:   cachePath,
 | |
| 		fwrite:  fwriteFile(),
 | |
| 	}
 | |
| 	return this.webdavFile, nil
 | |
| }
 | |
| 
 | |
| func (this WebdavFs) RemoveAll(ctx context.Context, name string) error {
 | |
| 	if name = this.fullpath(name); name == "" {
 | |
| 		return os.ErrNotExist
 | |
| 	}
 | |
| 	return this.backend.Rm(name)
 | |
| }
 | |
| 
 | |
| func (this WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
 | |
| 	if oldName = this.fullpath(oldName); oldName == "" {
 | |
| 		return os.ErrNotExist
 | |
| 	} else if newName = this.fullpath(newName); newName == "" {
 | |
| 		return os.ErrNotExist
 | |
| 	}
 | |
| 	return this.backend.Mv(oldName, newName)
 | |
| }
 | |
| 
 | |
| func (this *WebdavFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
 | |
| 	if this.webdavFile != nil {
 | |
| 		this.webdavFile.push_to_remote_if_needed()
 | |
| 		return this.webdavFile.Stat()
 | |
| 	}
 | |
| 	fullname := this.fullpath(name)
 | |
| 	if fullname == "" {
 | |
| 		return nil, os.ErrNotExist
 | |
| 	}
 | |
| 	this.webdavFile = &WebdavFile{
 | |
| 		path:    fullname,
 | |
| 		backend: this.backend,
 | |
| 		cache:   fmt.Sprintf("%stmp_%s", cachePath, Hash(this.id+name, 20)),
 | |
| 	}
 | |
| 	return this.webdavFile.Stat()
 | |
| }
 | |
| 
 | |
| func (this WebdavFs) fullpath(path string) string {
 | |
| 	p := filepath.Join(this.chroot, path)
 | |
| 	if strings.HasSuffix(path, "/") == true && strings.HasSuffix(p, "/") == false {
 | |
| 		p += "/"
 | |
| 	}
 | |
| 	if strings.HasPrefix(p, this.chroot) == false {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Implement a webdav.File and os.Stat : https://godoc.org/golang.org/x/net/webdav#File
 | |
|  */
 | |
| type WebdavFile struct {
 | |
| 	path    string
 | |
| 	backend IBackend
 | |
| 	cache   string
 | |
| 	fread   *os.File
 | |
| 	fwrite  *os.File
 | |
| 	files   []os.FileInfo
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Read(p []byte) (n int, err error) {
 | |
| 	if strings.HasPrefix(filepath.Base(this.path), ".") {
 | |
| 		return 0, os.ErrNotExist
 | |
| 	}
 | |
| 	if this.fread == nil {
 | |
| 		if this.fread = this.pull_remote_file(); this.fread == nil {
 | |
| 			return -1, os.ErrInvalid
 | |
| 		}
 | |
| 	}
 | |
| 	return this.fread.Read(p)
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Close() error {
 | |
| 	if this.fread != nil {
 | |
| 		if this.fread.Close() == nil {
 | |
| 			this.fread = nil
 | |
| 		}
 | |
| 	}
 | |
| 	if this.fwrite != nil {
 | |
| 		if err := this.push_to_remote_if_needed(); err == nil {
 | |
| 			if this.fwrite.Close() == nil {
 | |
| 				this.fwrite = nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Seek(offset int64, whence int) (int64, error) {
 | |
| 	if this.fread == nil {
 | |
| 		this.fread = this.pull_remote_file()
 | |
| 		if this.fread == nil {
 | |
| 			return offset, ErrNotFound
 | |
| 		}
 | |
| 	}
 | |
| 	a, err := this.fread.Seek(offset, whence)
 | |
| 	if err != nil {
 | |
| 		return a, ErrNotFound
 | |
| 	}
 | |
| 	return a, nil
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Readdir(count int) ([]os.FileInfo, error) {
 | |
| 	if this.files != nil {
 | |
| 		return this.files, nil
 | |
| 	}
 | |
| 	if strings.HasPrefix(filepath.Base(this.path), ".") {
 | |
| 		return nil, os.ErrNotExist
 | |
| 	}
 | |
| 	f, err := this.backend.Ls(this.path)
 | |
| 	this.files = f
 | |
| 	return f, err
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Stat() (os.FileInfo, error) {
 | |
| 	this.push_to_remote_if_needed()
 | |
| 	if strings.HasSuffix(this.path, "/") {
 | |
| 		_, err := this.Readdir(0)
 | |
| 		if err != nil {
 | |
| 			return nil, os.ErrNotExist
 | |
| 		}
 | |
| 		return this, nil
 | |
| 	}
 | |
| 	baseDir := filepath.Base(this.path)
 | |
| 	files, err := this.backend.Ls(strings.TrimSuffix(this.path, baseDir))
 | |
| 	if err != nil {
 | |
| 		return nil, os.ErrNotExist
 | |
| 	}
 | |
| 	found := false
 | |
| 	for i := range files {
 | |
| 		if files[i].Name() == baseDir {
 | |
| 			found = true
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	if found == false {
 | |
| 		return nil, os.ErrNotExist
 | |
| 	}
 | |
| 	return this, nil
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Write(p []byte) (int, error) {
 | |
| 	if this.fwrite == nil {
 | |
| 		return 0, os.ErrNotExist
 | |
| 	}
 | |
| 	if strings.HasPrefix(filepath.Base(this.path), ".") {
 | |
| 		return 0, os.ErrNotExist
 | |
| 	}
 | |
| 	return this.fwrite.Write(p)
 | |
| }
 | |
| 
 | |
| func (this WebdavFile) pull_remote_file() *os.File {
 | |
| 	filename := this.cache + "_reader"
 | |
| 	if f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm); err == nil {
 | |
| 		return f
 | |
| 	}
 | |
| 	if f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err == nil {
 | |
| 		if reader, err := this.backend.Cat(this.path); err == nil {
 | |
| 			io.Copy(f, reader)
 | |
| 			f.Close()
 | |
| 			webdavCache.SetKey(this.cache+"_reader", nil)
 | |
| 			reader.Close()
 | |
| 			if f, err = os.OpenFile(filename, os.O_RDONLY, os.ModePerm); err == nil {
 | |
| 				return f
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 		f.Close()
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) push_to_remote_if_needed() error {
 | |
| 	if this.fwrite == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	this.fwrite.Close()
 | |
| 	f, err := os.OpenFile(this.cache+"_writer", os.O_RDONLY, os.ModePerm)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	err = this.backend.Save(this.path, f)
 | |
| 	if err == nil {
 | |
| 		if err = os.Rename(this.cache+"_writer", this.cache+"_reader"); err == nil {
 | |
| 			this.fwrite = nil
 | |
| 			webdavCache.SetKey(this.cache+"_reader", nil)
 | |
| 		}
 | |
| 	}
 | |
| 	f.Close()
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (this WebdavFile) Name() string {
 | |
| 	return filepath.Base(this.path)
 | |
| }
 | |
| 
 | |
| func (this *WebdavFile) Size() int64 {
 | |
| 	if this.fread == nil {
 | |
| 		if this.fread = this.pull_remote_file(); this.fread == nil {
 | |
| 			return 0
 | |
| 		}
 | |
| 	}
 | |
| 	if info, err := this.fread.Stat(); err == nil {
 | |
| 		return info.Size()
 | |
| 	}
 | |
| 	return 0
 | |
| }
 | |
| 
 | |
| func (this WebdavFile) Mode() os.FileMode {
 | |
| 	return 0
 | |
| }
 | |
| 
 | |
| func (this WebdavFile) ModTime() time.Time {
 | |
| 	return time.Now()
 | |
| }
 | |
| func (this WebdavFile) IsDir() bool {
 | |
| 	if strings.HasSuffix(this.path, "/") {
 | |
| 		return true
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (this WebdavFile) Sys() interface{} {
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (this WebdavFile) ETag(ctx context.Context) (string, error) {
 | |
| 	// Building an etag can be an expensive call if the data isn't available locally.
 | |
| 	// => 2 etags strategies:
 | |
| 	// - use a legit etag value when the data is already in our cache
 | |
| 	// - use a dummy value that's changing all the time when we don't have much info
 | |
| 
 | |
| 	etag := Hash(fmt.Sprintf("%d%s", this.ModTime().UnixNano(), this.path), 20)
 | |
| 	if this.fread != nil {
 | |
| 		if s, err := this.fread.Stat(); err == nil {
 | |
| 			etag = Hash(fmt.Sprintf(`"%x%x"`, this.path, s.Size()), 20)
 | |
| 		}
 | |
| 	}
 | |
| 	return etag, nil
 | |
| }
 | |
| 
 | |
| var lock webdav.LockSystem
 | |
| 
 | |
| func NewWebdavLock() webdav.LockSystem {
 | |
| 	if lock == nil {
 | |
| 		lock = webdav.NewMemLS()
 | |
| 	}
 | |
| 	return lock
 | |
| }
 | 
