Merge branch '12125-workbench-project-trash' refs #12125
[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 // A CollectionFileSystem is an http.Filesystem with an added Stat() method.
150 type CollectionFileSystem interface {
151         http.FileSystem
152         Stat(name string) (os.FileInfo, error)
153 }
154
155 // collectionFS implements CollectionFileSystem.
156 type collectionFS struct {
157         collection *Collection
158         client     *Client
159         kc         keepClient
160         sizes      map[string]int64
161         sizesOnce  sync.Once
162 }
163
164 // FileSystem returns a CollectionFileSystem for the collection.
165 func (c *Collection) FileSystem(client *Client, kc keepClient) CollectionFileSystem {
166         return &collectionFS{
167                 collection: c,
168                 client:     client,
169                 kc:         kc,
170         }
171 }
172
173 func (c *collectionFS) Stat(name string) (os.FileInfo, error) {
174         name = canonicalName(name)
175         if name == "." {
176                 return collectionDirent{
177                         collection: c.collection,
178                         name:       "/",
179                         isDir:      true,
180                 }, nil
181         }
182         if size, ok := c.fileSizes()[name]; ok {
183                 return collectionDirent{
184                         collection: c.collection,
185                         name:       path.Base(name),
186                         size:       size,
187                         isDir:      false,
188                 }, nil
189         }
190         for fnm := range c.fileSizes() {
191                 if !strings.HasPrefix(fnm, name+"/") {
192                         continue
193                 }
194                 return collectionDirent{
195                         collection: c.collection,
196                         name:       path.Base(name),
197                         isDir:      true,
198                 }, nil
199         }
200         return nil, os.ErrNotExist
201 }
202
203 func (c *collectionFS) Open(name string) (http.File, error) {
204         // Ensure name looks the way it does in a manifest.
205         name = canonicalName(name)
206
207         m := manifest.Manifest{Text: c.collection.ManifestText}
208
209         // Return a file if it exists.
210         if size, ok := c.fileSizes()[name]; ok {
211                 reader, err := c.kc.ManifestFileReader(m, name)
212                 if err != nil {
213                         return nil, err
214                 }
215                 return &collectionFile{
216                         File:       reader,
217                         collection: c.collection,
218                         name:       path.Base(name),
219                         size:       size,
220                 }, nil
221         }
222
223         // Return a directory if it's the root dir or there are file
224         // entries below it.
225         children := map[string]collectionDirent{}
226         for fnm, size := range c.fileSizes() {
227                 if !strings.HasPrefix(fnm, name+"/") {
228                         continue
229                 }
230                 isDir := false
231                 ent := fnm[len(name)+1:]
232                 if i := strings.Index(ent, "/"); i >= 0 {
233                         ent = ent[:i]
234                         isDir = true
235                 }
236                 e := children[ent]
237                 e.collection = c.collection
238                 e.isDir = isDir
239                 e.name = ent
240                 e.size = size
241                 children[ent] = e
242         }
243         if len(children) == 0 && name != "." {
244                 return nil, os.ErrNotExist
245         }
246         dirents := make([]os.FileInfo, 0, len(children))
247         for _, ent := range children {
248                 dirents = append(dirents, ent)
249         }
250         return &collectionDir{
251                 collection: c.collection,
252                 stream:     name,
253                 dirents:    dirents,
254         }, nil
255 }
256
257 // fileSizes returns a map of files that can be opened. Each key
258 // starts with "./".
259 func (c *collectionFS) fileSizes() map[string]int64 {
260         c.sizesOnce.Do(func() {
261                 c.sizes = map[string]int64{}
262                 m := manifest.Manifest{Text: c.collection.ManifestText}
263                 for ms := range m.StreamIter() {
264                         for _, fss := range ms.FileStreamSegments {
265                                 c.sizes[ms.StreamName+"/"+fss.Name] += int64(fss.SegLen)
266                         }
267                 }
268         })
269         return c.sizes
270 }
271
272 func canonicalName(name string) string {
273         name = path.Clean("/" + name)
274         if name == "/" || name == "./" {
275                 name = "."
276         } else if strings.HasPrefix(name, "/") {
277                 name = "." + name
278         }
279         return name
280 }