20318: Merge branch 'main' into 20318-disk-cache
[arvados.git] / lib / webdavfs / fs.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 // Package webdavfs adds special behaviors to an arvados.FileSystem so
6 // it's suitable to use with a webdav server.
7 package webdavfs
8
9 import (
10         "context"
11         "crypto/rand"
12         "errors"
13         "fmt"
14         "io"
15         prand "math/rand"
16         "os"
17         "strings"
18         "sync/atomic"
19         "time"
20
21         "git.arvados.org/arvados.git/sdk/go/arvados"
22         "golang.org/x/net/webdav"
23 )
24
25 var (
26         lockPrefix     string = uuid()
27         nextLockSuffix int64  = prand.Int63()
28         ErrReadOnly           = errors.New("read-only filesystem")
29 )
30
31 // FS implements a webdav.FileSystem by wrapping an
32 // arvados.CollectionFilesystem.
33 type FS struct {
34         FileSystem arvados.FileSystem
35         // Prefix works like fs.Sub: Stat(name) calls
36         // Stat(prefix+name) in the wrapped filesystem.
37         Prefix string
38         // If Writing is false, all write operations return errors.
39         // (Opening a file for writing succeeds -- otherwise webdav
40         // would return 404 -- but writing to it fails.)
41         Writing bool
42         // webdav PROPFIND reads the first few bytes of each file
43         // whose filename extension isn't recognized, which is
44         // prohibitively expensive: we end up fetching multiple 64MiB
45         // blocks. Avoid this by returning EOF on all reads when
46         // handling a PROPFIND.
47         AlwaysReadEOF bool
48 }
49
50 func (fs *FS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
51         if !fs.Writing {
52                 return ErrReadOnly
53         }
54         name = strings.TrimRight(name, "/")
55         return fs.FileSystem.Mkdir(fs.Prefix+name, 0755)
56 }
57
58 func (fs *FS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (f webdav.File, err error) {
59         writing := flag&(os.O_WRONLY|os.O_RDWR|os.O_TRUNC) != 0
60         f, err = fs.FileSystem.OpenFile(fs.Prefix+name, flag, perm)
61         if !fs.Writing {
62                 // webdav module returns 404 on all OpenFile errors,
63                 // but returns 405 Method Not Allowed if OpenFile()
64                 // succeeds but Write() or Close() fails. We'd rather
65                 // have 405. writeFailer ensures Close() fails if the
66                 // file is opened for writing *or* Write() is called.
67                 var err error
68                 if writing {
69                         err = ErrReadOnly
70                 }
71                 f = writeFailer{File: f, err: err}
72         }
73         if fs.AlwaysReadEOF {
74                 f = readEOF{File: f}
75         }
76         return
77 }
78
79 func (fs *FS) RemoveAll(ctx context.Context, name string) error {
80         return fs.FileSystem.RemoveAll(fs.Prefix + name)
81 }
82
83 func (fs *FS) Rename(ctx context.Context, oldName, newName string) error {
84         if !fs.Writing {
85                 return ErrReadOnly
86         }
87         if strings.HasSuffix(oldName, "/") {
88                 // WebDAV "MOVE foo/ bar/" means rename foo to bar.
89                 oldName = oldName[:len(oldName)-1]
90                 newName = strings.TrimSuffix(newName, "/")
91         }
92         return fs.FileSystem.Rename(fs.Prefix+oldName, fs.Prefix+newName)
93 }
94
95 func (fs *FS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
96         return fs.FileSystem.Stat(fs.Prefix + name)
97 }
98
99 type writeFailer struct {
100         webdav.File
101         err error
102 }
103
104 func (wf writeFailer) Write([]byte) (int, error) {
105         wf.err = ErrReadOnly
106         return 0, wf.err
107 }
108
109 func (wf writeFailer) Close() error {
110         err := wf.File.Close()
111         if err != nil {
112                 wf.err = err
113         }
114         return wf.err
115 }
116
117 type readEOF struct {
118         webdav.File
119 }
120
121 func (readEOF) Read(p []byte) (int, error) {
122         return 0, io.EOF
123 }
124
125 // NoLockSystem implements webdav.LockSystem by returning success for
126 // every possible locking operation, even though it has no side
127 // effects such as actually locking anything. This works for a
128 // read-only webdav filesystem because webdav locks only apply to
129 // writes.
130 //
131 // This is more suitable than webdav.NewMemLS() for two reasons:
132 // First, it allows keep-web to use one locker for all collections
133 // even though coll1.vhost/foo and coll2.vhost/foo have the same path
134 // but represent different resources. Additionally, it returns valid
135 // tokens (rfc2518 specifies that tokens are represented as URIs and
136 // are unique across all resources for all time), which might improve
137 // client compatibility.
138 //
139 // However, it does also permit impossible operations, like acquiring
140 // conflicting locks and releasing non-existent locks.  This might
141 // confuse some clients if they try to probe for correctness.
142 //
143 // Currently this is a moot point: the LOCK and UNLOCK methods are not
144 // accepted by keep-web, so it suffices to implement the
145 // webdav.LockSystem interface.
146 var NoLockSystem = noLockSystem{}
147
148 type noLockSystem struct{}
149
150 func (noLockSystem) Confirm(time.Time, string, string, ...webdav.Condition) (func(), error) {
151         return noop, nil
152 }
153
154 func (noLockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) {
155         return fmt.Sprintf("opaquelocktoken:%s-%x", lockPrefix, atomic.AddInt64(&nextLockSuffix, 1)), nil
156 }
157
158 func (noLockSystem) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) {
159         return webdav.LockDetails{}, nil
160 }
161
162 func (noLockSystem) Unlock(now time.Time, token string) error {
163         return nil
164 }
165
166 func noop() {}
167
168 // Return a version 1 variant 4 UUID, meaning all bits are random
169 // except the ones indicating the version and variant.
170 func uuid() string {
171         var data [16]byte
172         if _, err := rand.Read(data[:]); err != nil {
173                 panic(err)
174         }
175         // variant 1: N=10xx
176         data[8] = data[8]&0x3f | 0x80
177         // version 4: M=0100
178         data[6] = data[6]&0x0f | 0x40
179         return fmt.Sprintf("%x-%x-%x-%x-%x", data[0:4], data[4:6], data[6:8], data[8:10], data[10:])
180 }