1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
15 type CustomFileSystem interface {
17 MountByID(mount string)
18 MountProject(mount, uuid string)
19 MountUsers(mount string)
20 ForwardSlashNameSubstitution(string)
23 type customFileSystem struct {
28 staleThreshold time.Time
31 forwardSlashNameSubstitution string
34 func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
36 fs := &customFileSystem{
38 fileSystem: fileSystem{
39 fsBackend: keepBackend{apiClient: c, keepClient: kc},
41 thr: newThrottle(concurrentWriters),
44 root.treenode = treenode{
49 mode: os.ModeDir | 0755,
52 inodes: make(map[string]inode),
57 func (fs *customFileSystem) MountByID(mount string) {
58 fs.root.treenode.Lock()
59 defer fs.root.treenode.Unlock()
60 fs.root.treenode.Child(mount, func(inode) (inode, error) {
65 inodes: make(map[string]inode),
69 mode: 0755 | os.ModeDir,
77 func (fs *customFileSystem) MountProject(mount, uuid string) {
78 fs.root.treenode.Lock()
79 defer fs.root.treenode.Unlock()
80 fs.root.treenode.Child(mount, func(inode) (inode, error) {
81 return fs.newProjectNode(fs.root, mount, uuid, nil), nil
85 func (fs *customFileSystem) MountUsers(mount string) {
86 fs.root.treenode.Lock()
87 defer fs.root.treenode.Unlock()
88 fs.root.treenode.Child(mount, func(inode) (inode, error) {
91 loadOne: fs.usersLoadOne,
92 loadAll: fs.usersLoadAll,
96 inodes: make(map[string]inode),
100 mode: 0755 | os.ModeDir,
107 func (fs *customFileSystem) ForwardSlashNameSubstitution(repl string) {
108 fs.forwardSlashNameSubstitution = repl
111 // SiteFileSystem returns a FileSystem that maps collections and other
112 // Arvados objects onto a filesystem layout.
114 // This is experimental: the filesystem layout is not stable, and
115 // there are significant known bugs and shortcomings. For example,
116 // writes are not persisted until Sync() is called.
117 func (c *Client) SiteFileSystem(kc keepClient) CustomFileSystem {
118 fs := c.CustomFileSystem(kc)
119 fs.MountByID("by_id")
120 fs.MountUsers("users")
124 func (fs *customFileSystem) Sync() error {
125 return fs.root.Sync()
128 // Stale returns true if information obtained at time t should be
130 func (fs *customFileSystem) Stale(t time.Time) bool {
132 defer fs.staleLock.Unlock()
133 return !fs.staleThreshold.Before(t)
136 func (fs *customFileSystem) newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error) {
137 return nil, ErrInvalidOperation
140 func (fs *customFileSystem) mountByID(parent inode, id string) (inode, error) {
141 if strings.Contains(id, "-4zz18-") || pdhRegexp.MatchString(id) {
142 return fs.mountCollection(parent, id)
143 } else if strings.Contains(id, "-j7d0g-") {
144 return fs.newProjectNode(fs.root, id, id, nil), nil
150 func (fs *customFileSystem) mountCollection(parent inode, id string) (inode, error) {
152 err := fs.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+id, nil, nil)
153 if statusErr, ok := err.(interface{ HTTPStatus() int }); ok && statusErr.HTTPStatus() == http.StatusNotFound {
155 } else if err != nil {
159 // This means id is a PDH, and controller/railsapi
160 // returned one of (possibly) many collections with
161 // that PDH. Even if controller returns more fields
162 // besides PDH and manifest text (which are equal for
163 // all matching collections), we don't want to expose
164 // them (e.g., through Sys()).
166 PortableDataHash: coll.PortableDataHash,
167 ManifestText: coll.ManifestText,
170 newfs, err := coll.FileSystem(fs, fs)
174 cfs := newfs.(*collectionFileSystem)
175 cfs.SetParent(parent, id)
179 func (fs *customFileSystem) newProjectNode(root inode, name, uuid string, proj *Group) inode {
180 var projLoading sync.Mutex
183 loadOne: func(parent inode, name string) (inode, error) { return fs.projectsLoadOne(parent, uuid, name) },
184 loadAll: func(parent inode) ([]inode, error) { return fs.projectsLoadAll(parent, uuid) },
188 inodes: make(map[string]inode),
192 mode: 0755 | os.ModeDir,
193 sys: func() interface{} {
195 defer projLoading.Unlock()
200 err := fs.RequestAndDecode(&g, "GET", "arvados/v1/groups/"+uuid, nil, nil)
212 // vdirnode wraps an inode by rejecting (with ErrInvalidOperation)
213 // calls that add/replace children directly, instead calling a
214 // create() func when a non-existing child is looked up.
216 // create() can return either a new node, which will be added to the
217 // treenode, or nil for ENOENT.
218 type vdirnode struct {
220 create func(parent inode, name string) (inode, error)
223 func (vn *vdirnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
224 return vn.treenode.Child(name, func(existing inode) (inode, error) {
225 if existing == nil && vn.create != nil {
226 newnode, err := vn.create(vn, name)
231 newnode.SetParent(vn, name)
233 vn.treenode.fileinfo.modTime = time.Now()
238 } else if tryRepl, err := replace(existing); err != nil {
240 } else if tryRepl != existing {
241 return existing, ErrInvalidOperation