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