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