12216: Add webdav handler.
[arvados.git] / sdk / go / arvados / collection_fs.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "io"
9         "net/http"
10         "os"
11         "path"
12         "strings"
13         "sync"
14         "time"
15
16         "git.curoverse.com/arvados.git/sdk/go/manifest"
17 )
18
19 type File interface {
20         io.Reader
21         io.Closer
22         io.Seeker
23         Size() int64
24 }
25
26 type keepClient interface {
27         ManifestFileReader(manifest.Manifest, string) (File, error)
28 }
29
30 type collectionFile struct {
31         File
32         collection *Collection
33         name       string
34         size       int64
35 }
36
37 func (cf *collectionFile) Size() int64 {
38         return cf.size
39 }
40
41 func (cf *collectionFile) Readdir(count int) ([]os.FileInfo, error) {
42         return nil, io.EOF
43 }
44
45 func (cf *collectionFile) Stat() (os.FileInfo, error) {
46         return collectionDirent{
47                 collection: cf.collection,
48                 name:       cf.name,
49                 size:       cf.size,
50                 isDir:      false,
51         }, nil
52 }
53
54 type collectionDir struct {
55         collection *Collection
56         stream     string
57         dirents    []os.FileInfo
58 }
59
60 // Readdir implements os.File.
61 func (cd *collectionDir) Readdir(count int) ([]os.FileInfo, error) {
62         ret := cd.dirents
63         if count <= 0 {
64                 cd.dirents = nil
65                 return ret, nil
66         } else if len(ret) == 0 {
67                 return nil, io.EOF
68         }
69         var err error
70         if count >= len(ret) {
71                 count = len(ret)
72                 err = io.EOF
73         }
74         cd.dirents = cd.dirents[count:]
75         return ret[:count], err
76 }
77
78 // Stat implements os.File.
79 func (cd *collectionDir) Stat() (os.FileInfo, error) {
80         return collectionDirent{
81                 collection: cd.collection,
82                 name:       path.Base(cd.stream),
83                 isDir:      true,
84                 size:       int64(len(cd.dirents)),
85         }, nil
86 }
87
88 // Close implements os.File.
89 func (cd *collectionDir) Close() error {
90         return nil
91 }
92
93 // Read implements os.File.
94 func (cd *collectionDir) Read([]byte) (int, error) {
95         return 0, nil
96 }
97
98 // Seek implements os.File.
99 func (cd *collectionDir) Seek(int64, int) (int64, error) {
100         return 0, nil
101 }
102
103 // collectionDirent implements os.FileInfo.
104 type collectionDirent struct {
105         collection *Collection
106         name       string
107         isDir      bool
108         mode       os.FileMode
109         size       int64
110 }
111
112 // Name implements os.FileInfo.
113 func (e collectionDirent) Name() string {
114         return e.name
115 }
116
117 // ModTime implements os.FileInfo.
118 func (e collectionDirent) ModTime() time.Time {
119         if e.collection.ModifiedAt == nil {
120                 return time.Now()
121         }
122         return *e.collection.ModifiedAt
123 }
124
125 // Mode implements os.FileInfo.
126 func (e collectionDirent) Mode() os.FileMode {
127         if e.isDir {
128                 return 0555
129         } else {
130                 return 0444
131         }
132 }
133
134 // IsDir implements os.FileInfo.
135 func (e collectionDirent) IsDir() bool {
136         return e.isDir
137 }
138
139 // Size implements os.FileInfo.
140 func (e collectionDirent) Size() int64 {
141         return e.size
142 }
143
144 // Sys implements os.FileInfo.
145 func (e collectionDirent) Sys() interface{} {
146         return nil
147 }
148
149 // collectionFS implements http.FileSystem.
150 type collectionFS struct {
151         collection *Collection
152         client     *Client
153         kc         keepClient
154         sizes      map[string]int64
155         sizesOnce  sync.Once
156 }
157
158 // FileSystem returns an http.FileSystem for the collection.
159 func (c *Collection) FileSystem(client *Client, kc keepClient) http.FileSystem {
160         return &collectionFS{
161                 collection: c,
162                 client:     client,
163                 kc:         kc,
164         }
165 }
166
167 func (c *collectionFS) Open(name string) (http.File, error) {
168         // Ensure name looks the way it does in a manifest.
169         name = path.Clean("/" + name)
170         if name == "/" || name == "./" {
171                 name = "."
172         } else if strings.HasPrefix(name, "/") {
173                 name = "." + name
174         }
175
176         m := manifest.Manifest{Text: c.collection.ManifestText}
177
178         filesizes := c.fileSizes()
179
180         // Return a file if it exists.
181         if size, ok := filesizes[name]; ok {
182                 reader, err := c.kc.ManifestFileReader(m, name)
183                 if err != nil {
184                         return nil, err
185                 }
186                 return &collectionFile{
187                         File:       reader,
188                         collection: c.collection,
189                         name:       path.Base(name),
190                         size:       size,
191                 }, nil
192         }
193
194         // Return a directory if it's the root dir or there are file
195         // entries below it.
196         children := map[string]collectionDirent{}
197         for fnm, size := range filesizes {
198                 if !strings.HasPrefix(fnm, name+"/") {
199                         continue
200                 }
201                 isDir := false
202                 ent := fnm[len(name)+1:]
203                 if i := strings.Index(ent, "/"); i >= 0 {
204                         ent = ent[:i]
205                         isDir = true
206                 }
207                 e := children[ent]
208                 e.collection = c.collection
209                 e.isDir = isDir
210                 e.name = ent
211                 e.size = size
212                 children[ent] = e
213         }
214         if len(children) == 0 && name != "." {
215                 return nil, os.ErrNotExist
216         }
217         dirents := make([]os.FileInfo, 0, len(children))
218         for _, ent := range children {
219                 dirents = append(dirents, ent)
220         }
221         return &collectionDir{
222                 collection: c.collection,
223                 stream:     name,
224                 dirents:    dirents,
225         }, nil
226 }
227
228 // fileSizes returns a map of files that can be opened. Each key
229 // starts with "./".
230 func (c *collectionFS) fileSizes() map[string]int64 {
231         c.sizesOnce.Do(func() {
232                 c.sizes = map[string]int64{}
233                 m := manifest.Manifest{Text: c.collection.ManifestText}
234                 for ms := range m.StreamIter() {
235                         for _, fss := range ms.FileStreamSegments {
236                                 c.sizes[ms.StreamName+"/"+fss.Name] += int64(fss.SegLen)
237                         }
238                 }
239         })
240         return c.sizes
241 }