• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jstaf / onedriver / 6539229776

16 Oct 2023 09:05PM UTC coverage: 71.743% (-2.4%) from 74.097%
6539229776

Pull #357

github

jstaf
0.14.0-2 dummy release to force gpg key refresh on OBS
Pull Request #357: 0.14.0-2 dummy release to force gpg key refresh on OBS

1828 of 2548 relevant lines covered (71.74%)

172113.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.85
/fs/fs.go
1
package fs
2

3
import (
4
        "math"
5
        "os"
6
        "path/filepath"
7
        "regexp"
8
        "strings"
9
        "syscall"
10
        "time"
11

12
        "github.com/hanwen/go-fuse/v2/fuse"
13
        "github.com/jstaf/onedriver/fs/graph"
14
        "github.com/rs/zerolog/log"
15
)
16

17
const timeout = time.Second
18

19
func (f *Filesystem) getInodeContent(i *Inode) *[]byte {
53✔
20
        i.RLock()
53✔
21
        defer i.RUnlock()
53✔
22
        data := f.content.Get(i.DriveItem.ID)
53✔
23
        return &data
53✔
24
}
53✔
25

26
// remoteID uploads a file to obtain a Onedrive ID if it doesn't already
27
// have one. This is necessary to avoid race conditions against uploads if the
28
// file has not already been uploaded.
29
func (f *Filesystem) remoteID(i *Inode) (string, error) {
7✔
30
        if i.IsDir() {
7✔
31
                // Directories are always created with an ID. (And this method is only
×
32
                // really used for files anyways...)
×
33
                return i.ID(), nil
×
34
        }
×
35

36
        originalID := i.ID()
7✔
37
        if isLocalID(originalID) && f.auth.AccessToken != "" {
12✔
38
                // perform a blocking upload of the item
5✔
39
                data := f.getInodeContent(i)
5✔
40
                session, err := NewUploadSession(i, data)
5✔
41
                if err != nil {
5✔
42
                        return originalID, err
×
43
                }
×
44

45
                i.Lock()
5✔
46
                name := i.DriveItem.Name
5✔
47
                err = session.Upload(f.auth)
5✔
48
                if err != nil {
5✔
49
                        i.Unlock()
×
50

×
51
                        if strings.Contains(err.Error(), "nameAlreadyExists") {
×
52
                                // A file with this name already exists on the server, get its ID and
×
53
                                // use that. This is probably the same file, but just got uploaded
×
54
                                // earlier.
×
55
                                children, err := graph.GetItemChildren(i.ParentID(), f.auth)
×
56
                                if err != nil {
×
57
                                        return originalID, err
×
58
                                }
×
59
                                for _, child := range children {
×
60
                                        if child.Name == name {
×
61
                                                log.Info().
×
62
                                                        Str("name", name).
×
63
                                                        Str("originalID", originalID).
×
64
                                                        Str("newID", child.ID).
×
65
                                                        Msg("Exchanged ID.")
×
66
                                                return child.ID, f.MoveID(originalID, child.ID)
×
67
                                        }
×
68
                                }
69
                        }
70
                        // failed to obtain an ID, return whatever it was beforehand
71
                        return originalID, err
×
72
                }
73

74
                // we just successfully uploaded a copy, no need to do it again
75
                i.hasChanges = false
5✔
76
                i.DriveItem.ETag = session.ETag
5✔
77
                i.Unlock()
5✔
78

5✔
79
                // this is all we really wanted from this transaction
5✔
80
                err = f.MoveID(originalID, session.ID)
5✔
81
                log.Info().
5✔
82
                        Str("name", name).
5✔
83
                        Str("originalID", originalID).
5✔
84
                        Str("newID", session.ID).
5✔
85
                        Msg("Exchanged ID.")
5✔
86
                return session.ID, err
5✔
87
        }
88
        return originalID, nil
2✔
89
}
90

91
var disallowedRexp = regexp.MustCompile(`(?i)LPT[0-9]|COM[0-9]|_vti_|["*:<>?\/\\\|]`)
92

93
// isNameRestricted returns true if the name is disallowed according to the doc here:
94
// https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa
95
func isNameRestricted(name string) bool {
69✔
96
        if strings.EqualFold(name, "CON") {
69✔
97
                return true
×
98
        }
×
99
        if strings.EqualFold(name, "AUX") {
69✔
100
                return true
×
101
        }
×
102
        if strings.EqualFold(name, "PRN") {
69✔
103
                return true
×
104
        }
×
105
        if strings.EqualFold(name, "NUL") {
69✔
106
                return true
×
107
        }
×
108
        if strings.EqualFold(name, ".lock") {
69✔
109
                return true
×
110
        }
×
111
        if strings.EqualFold(name, "desktop.ini") {
70✔
112
                return true
1✔
113
        }
1✔
114
        return disallowedRexp.FindStringIndex(name) != nil
68✔
115
}
116

117
// Statfs returns information about the filesystem. Mainly useful for checking
118
// quotas and storage limits.
119
func (f *Filesystem) StatFs(cancel <-chan struct{}, in *fuse.InHeader, out *fuse.StatfsOut) fuse.Status {
2✔
120
        ctx := log.With().Str("op", "StatFs").Logger()
2✔
121
        ctx.Debug().Msg("")
2✔
122
        drive, err := graph.GetDrive(f.auth)
2✔
123
        if err != nil {
2✔
124
                return fuse.EREMOTEIO
×
125
        }
×
126

127
        if drive.DriveType == graph.DriveTypePersonal {
4✔
128
                ctx.Warn().Msg("Personal OneDrive accounts do not show number of files, " +
2✔
129
                        "inode counts reported by onedriver will be bogus.")
2✔
130
        } else if drive.Quota.Total == 0 { // <-- check for if microsoft ever fixes their API
2✔
131
                ctx.Warn().Msg("OneDrive for Business accounts do not report quotas, " +
×
132
                        "pretending the quota is 5TB and it's all unused.")
×
133
                drive.Quota.Total = 5 * uint64(math.Pow(1024, 4))
×
134
                drive.Quota.Remaining = 5 * uint64(math.Pow(1024, 4))
×
135
                drive.Quota.FileCount = 0
×
136
        }
×
137

138
        // limits are pasted from https://support.microsoft.com/en-us/help/3125202
139
        const blkSize uint64 = 4096 // default ext4 block size
2✔
140
        out.Bsize = uint32(blkSize)
2✔
141
        out.Blocks = drive.Quota.Total / blkSize
2✔
142
        out.Bfree = drive.Quota.Remaining / blkSize
2✔
143
        out.Bavail = drive.Quota.Remaining / blkSize
2✔
144
        out.Files = 100000
2✔
145
        out.Ffree = 100000 - drive.Quota.FileCount
2✔
146
        out.NameLen = 260
2✔
147
        return fuse.OK
2✔
148
}
149

150
// Mkdir creates a directory.
151
func (f *Filesystem) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out *fuse.EntryOut) fuse.Status {
16✔
152
        if isNameRestricted(name) {
20✔
153
                return fuse.EINVAL
4✔
154
        }
4✔
155

156
        inode := f.GetNodeID(in.NodeId)
12✔
157
        if inode == nil {
12✔
158
                return fuse.ENOENT
×
159
        }
×
160
        id := inode.ID()
12✔
161
        path := filepath.Join(inode.Path(), name)
12✔
162
        ctx := log.With().
12✔
163
                Str("op", "Mkdir").
12✔
164
                Uint64("nodeID", in.NodeId).
12✔
165
                Str("id", id).
12✔
166
                Str("path", path).
12✔
167
                Str("mode", Octal(in.Mode)).
12✔
168
                Logger()
12✔
169
        ctx.Debug().Msg("")
12✔
170

12✔
171
        // create the new directory on the server
12✔
172
        item, err := graph.Mkdir(name, id, f.auth)
12✔
173
        if err != nil {
12✔
174
                ctx.Error().Err(err).Msg("Could not create remote directory!")
×
175
                return fuse.EREMOTEIO
×
176
        }
×
177

178
        newInode := NewInodeDriveItem(item)
12✔
179
        newInode.mode = in.Mode | fuse.S_IFDIR
12✔
180

12✔
181
        out.NodeId = f.InsertChild(id, newInode)
12✔
182
        out.Attr = newInode.makeAttr()
12✔
183
        out.SetAttrTimeout(timeout)
12✔
184
        out.SetEntryTimeout(timeout)
12✔
185
        return fuse.OK
12✔
186
}
187

188
// Rmdir removes a directory if it's empty.
189
func (f *Filesystem) Rmdir(cancel <-chan struct{}, in *fuse.InHeader, name string) fuse.Status {
6✔
190
        parentID := f.TranslateID(in.NodeId)
6✔
191
        if parentID == "" {
6✔
192
                return fuse.ENOENT
×
193
        }
×
194
        child, _ := f.GetChild(parentID, name, f.auth)
6✔
195
        if child == nil {
6✔
196
                return fuse.ENOENT
×
197
        }
×
198
        if child.HasChildren() {
8✔
199
                return fuse.Status(syscall.ENOTEMPTY)
2✔
200
        }
2✔
201
        return f.Unlink(cancel, in, name)
4✔
202
}
203

204
// ReadDir provides a list of all the entries in the directory
205
func (f *Filesystem) OpenDir(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) fuse.Status {
69✔
206
        id := f.TranslateID(in.NodeId)
69✔
207
        dir := f.GetID(id)
69✔
208
        if dir == nil {
69✔
209
                return fuse.ENOENT
×
210
        }
×
211
        if !dir.IsDir() {
69✔
212
                return fuse.ENOTDIR
×
213
        }
×
214
        path := dir.Path()
69✔
215
        ctx := log.With().
69✔
216
                Str("op", "OpenDir").
69✔
217
                Uint64("nodeID", in.NodeId).
69✔
218
                Str("id", id).
69✔
219
                Str("path", path).Logger()
69✔
220
        ctx.Debug().Msg("")
69✔
221

69✔
222
        children, err := f.GetChildrenID(id, f.auth)
69✔
223
        if err != nil {
69✔
224
                // not an item not found error (Lookup/Getattr will always be called
×
225
                // before Readdir()), something has happened to our connection
×
226
                ctx.Error().Err(err).Msg("Could not fetch children")
×
227
                return fuse.EREMOTEIO
×
228
        }
×
229

230
        parent := f.GetID(dir.ParentID())
69✔
231
        if parent == nil {
73✔
232
                // This is the parent of the mountpoint. The FUSE kernel module discards
4✔
233
                // this info, so what we put here doesn't actually matter.
4✔
234
                parent = NewInode("..", 0755|fuse.S_IFDIR, nil)
4✔
235
                parent.nodeID = math.MaxUint64
4✔
236
        }
4✔
237

238
        entries := make([]*Inode, 2)
69✔
239
        entries[0] = dir
69✔
240
        entries[1] = parent
69✔
241

69✔
242
        for _, child := range children {
578✔
243
                entries = append(entries, child)
509✔
244
        }
509✔
245
        f.opendirsM.Lock()
69✔
246
        f.opendirs[in.NodeId] = entries
69✔
247
        f.opendirsM.Unlock()
69✔
248

69✔
249
        return fuse.OK
69✔
250
}
251

252
// ReleaseDir closes a directory and purges it from memory
253
func (f *Filesystem) ReleaseDir(in *fuse.ReleaseIn) {
69✔
254
        f.opendirsM.Lock()
69✔
255
        delete(f.opendirs, in.NodeId)
69✔
256
        f.opendirsM.Unlock()
69✔
257
}
69✔
258

259
// ReadDirPlus reads an individual directory entry AND does a lookup.
260
func (f *Filesystem) ReadDirPlus(cancel <-chan struct{}, in *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status {
681✔
261
        f.opendirsM.RLock()
681✔
262
        entries, ok := f.opendirs[in.NodeId]
681✔
263
        f.opendirsM.RUnlock()
681✔
264
        if !ok {
681✔
265
                // readdir can sometimes arrive before the corresponding opendir, so we force it
×
266
                f.OpenDir(cancel, &fuse.OpenIn{InHeader: in.InHeader}, nil)
×
267
                f.opendirsM.RLock()
×
268
                entries, ok = f.opendirs[in.NodeId]
×
269
                f.opendirsM.RUnlock()
×
270
                if !ok {
×
271
                        return fuse.EBADF
×
272
                }
×
273
        }
274

275
        if in.Offset >= uint64(len(entries)) {
748✔
276
                // just tried to seek past end of directory, we're all done!
67✔
277
                return fuse.OK
67✔
278
        }
67✔
279

280
        inode := entries[in.Offset]
614✔
281
        entry := fuse.DirEntry{
614✔
282
                Ino:  inode.NodeID(),
614✔
283
                Mode: inode.Mode(),
614✔
284
        }
614✔
285
        // first two entries will always be "." and ".."
614✔
286
        switch in.Offset {
614✔
287
        case 0:
67✔
288
                entry.Name = "."
67✔
289
        case 1:
67✔
290
                entry.Name = ".."
67✔
291
        default:
480✔
292
                entry.Name = inode.Name()
480✔
293
        }
294
        entryOut := out.AddDirLookupEntry(entry)
614✔
295
        if entryOut == nil {
614✔
296
                //FIXME probably need to handle this better using the "overflow stuff"
×
297
                log.Error().
×
298
                        Str("op", "ReadDirPlus").
×
299
                        Uint64("nodeID", in.NodeId).
×
300
                        Uint64("offset", in.Offset).
×
301
                        Str("entryName", entry.Name).
×
302
                        Uint64("entryNodeID", entry.Ino).
×
303
                        Msg("Exceeded DirLookupEntry bounds!")
×
304
                return fuse.EIO
×
305
        }
×
306
        entryOut.NodeId = entry.Ino
614✔
307
        entryOut.Attr = inode.makeAttr()
614✔
308
        entryOut.SetAttrTimeout(timeout)
614✔
309
        entryOut.SetEntryTimeout(timeout)
614✔
310
        return fuse.OK
614✔
311
}
312

313
// ReadDir reads a directory entry. Usually doesn't get called (ReadDirPlus is
314
// typically used).
315
func (f *Filesystem) ReadDir(cancel <-chan struct{}, in *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status {
×
316
        f.opendirsM.RLock()
×
317
        entries, ok := f.opendirs[in.NodeId]
×
318
        f.opendirsM.RUnlock()
×
319
        if !ok {
×
320
                // readdir can sometimes arrive before the corresponding opendir, so we force it
×
321
                f.OpenDir(cancel, &fuse.OpenIn{InHeader: in.InHeader}, nil)
×
322
                f.opendirsM.RLock()
×
323
                entries, ok = f.opendirs[in.NodeId]
×
324
                f.opendirsM.RUnlock()
×
325
                if !ok {
×
326
                        return fuse.EBADF
×
327
                }
×
328
        }
329

330
        if in.Offset >= uint64(len(entries)) {
×
331
                // just tried to seek past end of directory, we're all done!
×
332
                return fuse.OK
×
333
        }
×
334

335
        inode := entries[in.Offset]
×
336
        entry := fuse.DirEntry{
×
337
                Ino:  inode.NodeID(),
×
338
                Mode: inode.Mode(),
×
339
        }
×
340
        // first two entries will always be "." and ".."
×
341
        switch in.Offset {
×
342
        case 0:
×
343
                entry.Name = "."
×
344
        case 1:
×
345
                entry.Name = ".."
×
346
        default:
×
347
                entry.Name = inode.Name()
×
348
        }
349

350
        out.AddDirEntry(entry)
×
351
        return fuse.OK
×
352
}
353

354
// Lookup is called by the kernel when the VFS wants to know about a file inside
355
// a directory.
356
func (f *Filesystem) Lookup(cancel <-chan struct{}, in *fuse.InHeader, name string, out *fuse.EntryOut) fuse.Status {
604✔
357
        id := f.TranslateID(in.NodeId)
604✔
358
        log.Trace().
604✔
359
                Str("op", "Lookup").
604✔
360
                Uint64("nodeID", in.NodeId).
604✔
361
                Str("id", id).
604✔
362
                Str("name", name).
604✔
363
                Msg("")
604✔
364

604✔
365
        child, _ := f.GetChild(id, strings.ToLower(name), f.auth)
604✔
366
        if child == nil {
756✔
367
                return fuse.ENOENT
152✔
368
        }
152✔
369

370
        out.NodeId = child.NodeID()
452✔
371
        out.Attr = child.makeAttr()
452✔
372
        out.SetAttrTimeout(timeout)
452✔
373
        out.SetEntryTimeout(timeout)
452✔
374
        return fuse.OK
452✔
375
}
376

377
// Mknod creates a regular file. The server doesn't have this yet.
378
func (f *Filesystem) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) fuse.Status {
45✔
379
        if isNameRestricted(name) {
49✔
380
                return fuse.EINVAL
4✔
381
        }
4✔
382

383
        parentID := f.TranslateID(in.NodeId)
41✔
384
        if parentID == "" {
41✔
385
                return fuse.EBADF
×
386
        }
×
387

388
        parent := f.GetID(parentID)
41✔
389
        if parent == nil {
41✔
390
                return fuse.ENOENT
×
391
        }
×
392

393
        path := filepath.Join(parent.Path(), name)
41✔
394
        ctx := log.With().
41✔
395
                Str("op", "Mknod").
41✔
396
                Uint64("nodeID", in.NodeId).
41✔
397
                Str("path", path).
41✔
398
                Logger()
41✔
399
        if f.IsOffline() {
41✔
400
                ctx.Warn().Msg("We are offline. Refusing Mknod() to avoid data loss later.")
×
401
                return fuse.EROFS
×
402
        }
×
403

404
        if child, _ := f.GetChild(parentID, name, f.auth); child != nil {
42✔
405
                return fuse.Status(syscall.EEXIST)
1✔
406
        }
1✔
407

408
        inode := NewInode(name, in.Mode, parent)
40✔
409
        ctx.Debug().
40✔
410
                Str("childID", inode.ID()).
40✔
411
                Str("mode", Octal(in.Mode)).
40✔
412
                Msg("Creating inode.")
40✔
413
        out.NodeId = f.InsertChild(parentID, inode)
40✔
414
        out.Attr = inode.makeAttr()
40✔
415
        out.SetAttrTimeout(timeout)
40✔
416
        out.SetEntryTimeout(timeout)
40✔
417
        return fuse.OK
40✔
418
}
419

420
// Create creates a regular file and opens it. The server doesn't have this yet.
421
func (f *Filesystem) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) fuse.Status {
45✔
422
        // we reuse mknod here
45✔
423
        result := f.Mknod(
45✔
424
                cancel,
45✔
425
                // we don't actually use the umask or padding here, so they don't get passed
45✔
426
                &fuse.MknodIn{
45✔
427
                        InHeader: in.InHeader,
45✔
428
                        Mode:     in.Mode,
45✔
429
                },
45✔
430
                name,
45✔
431
                &out.EntryOut,
45✔
432
        )
45✔
433
        if result == fuse.Status(syscall.EEXIST) {
46✔
434
                // if the inode already exists, we should truncate the existing file and
1✔
435
                // return the existing file inode as per "man creat"
1✔
436
                parentID := f.TranslateID(in.NodeId)
1✔
437
                child, _ := f.GetChild(parentID, name, f.auth)
1✔
438
                log.Debug().
1✔
439
                        Str("op", "Create").
1✔
440
                        Uint64("nodeID", in.NodeId).
1✔
441
                        Str("id", parentID).
1✔
442
                        Str("childID", child.ID()).
1✔
443
                        Str("path", child.Path()).
1✔
444
                        Str("mode", Octal(in.Mode)).
1✔
445
                        Msg("Child inode already exists, truncating.")
1✔
446
                f.content.Delete(child.ID())
1✔
447
                f.content.Open(child.ID())
1✔
448
                child.DriveItem.Size = 0
1✔
449
                child.hasChanges = true
1✔
450
                return fuse.OK
1✔
451
        }
1✔
452
        // no further initialized required to open the file, it's empty
453
        return result
44✔
454
}
455

456
// Open fetches a Inodes's content and initializes the .Data field with actual
457
// data from the server.
458
func (f *Filesystem) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) fuse.Status {
34✔
459
        id := f.TranslateID(in.NodeId)
34✔
460
        inode := f.GetID(id)
34✔
461
        if inode == nil {
34✔
462
                return fuse.ENOENT
×
463
        }
×
464

465
        path := inode.Path()
34✔
466
        ctx := log.With().
34✔
467
                Str("op", "Open").
34✔
468
                Uint64("nodeID", in.NodeId).
34✔
469
                Str("id", id).
34✔
470
                Str("path", path).
34✔
471
                Logger()
34✔
472

34✔
473
        flags := int(in.Flags)
34✔
474
        if flags&os.O_RDWR+flags&os.O_WRONLY > 0 && f.IsOffline() {
34✔
475
                ctx.Warn().
×
476
                        Bool("readWrite", flags&os.O_RDWR > 0).
×
477
                        Bool("writeOnly", flags&os.O_WRONLY > 0).
×
478
                        Msg("Refusing Open() with write flag, FS is offline.")
×
479
                return fuse.EROFS
×
480
        }
×
481

482
        ctx.Debug().Msg("")
34✔
483

34✔
484
        // try grabbing from disk
34✔
485
        fd, err := f.content.Open(id)
34✔
486
        if err != nil {
34✔
487
                ctx.Error().Err(err).Msg("Could not create cache file.")
×
488
                return fuse.EIO
×
489
        }
×
490

491
        if isLocalID(id) {
57✔
492
                // just use whatever's present if we're the only ones who have it
23✔
493
                return fuse.OK
23✔
494
        }
23✔
495

496
        // we have something on disk-
497
        // verify content against what we're supposed to have
498
        inode.Lock()
11✔
499
        defer inode.Unlock()
11✔
500
        // stay locked until end to prevent multiple Opens() from competing for
11✔
501
        // downloads of the same file.
11✔
502

11✔
503
        if inode.VerifyChecksum(graph.QuickXORHashStream(fd)) {
19✔
504
                // disk content is only used if the checksums match
8✔
505
                ctx.Info().Msg("Found content in cache.")
8✔
506

8✔
507
                // we check size ourselves in case the API file sizes are WRONG (it happens)
8✔
508
                st, _ := fd.Stat()
8✔
509
                inode.DriveItem.Size = uint64(st.Size())
8✔
510
                return fuse.OK
8✔
511
        }
8✔
512

513
        ctx.Info().Msg(
3✔
514
                "Not using cached item due to file hash mismatch, fetching content from API.",
3✔
515
        )
3✔
516
        size, err := graph.GetItemContentStream(id, f.auth, fd)
3✔
517
        if err != nil {
3✔
518
                ctx.Error().Err(err).Msg("Failed to fetch remote content.")
×
519
                return fuse.EREMOTEIO
×
520
        }
×
521
        inode.DriveItem.Size = size
3✔
522
        return fuse.OK
3✔
523
}
524

525
// Unlink deletes a child file.
526
func (f *Filesystem) Unlink(cancel <-chan struct{}, in *fuse.InHeader, name string) fuse.Status {
7✔
527
        parentID := f.TranslateID(in.NodeId)
7✔
528
        child, _ := f.GetChild(parentID, name, nil)
7✔
529
        if child == nil {
7✔
530
                // the file we are unlinking never existed
×
531
                return fuse.ENOENT
×
532
        }
×
533
        if f.IsOffline() {
7✔
534
                return fuse.EROFS
×
535
        }
×
536

537
        id := child.ID()
7✔
538
        path := child.Path()
7✔
539
        ctx := log.With().
7✔
540
                Str("op", "Unlink").
7✔
541
                Uint64("nodeID", in.NodeId).
7✔
542
                Str("id", parentID).
7✔
543
                Str("childID", id).
7✔
544
                Str("path", path).
7✔
545
                Logger()
7✔
546
        ctx.Debug().Msg("Unlinking inode.")
7✔
547

7✔
548
        // if no ID, the item is local-only, and does not need to be deleted on the
7✔
549
        // server
7✔
550
        if !isLocalID(id) {
11✔
551
                if err := graph.Remove(id, f.auth); err != nil {
4✔
552
                        ctx.Err(err).Msg("Failed to delete item on server. Aborting op.")
×
553
                        return fuse.EREMOTEIO
×
554
                }
×
555
        }
556

557
        f.DeleteID(id)
7✔
558
        f.content.Delete(id)
7✔
559
        return fuse.OK
7✔
560
}
561

562
// Read an inode's data like a file.
563
func (f *Filesystem) Read(cancel <-chan struct{}, in *fuse.ReadIn, buf []byte) (fuse.ReadResult, fuse.Status) {
205✔
564
        inode := f.GetNodeID(in.NodeId)
205✔
565
        if inode == nil {
205✔
566
                return fuse.ReadResultData(make([]byte, 0)), fuse.EBADF
×
567
        }
×
568

569
        id := inode.ID()
205✔
570
        path := inode.Path()
205✔
571
        ctx := log.With().
205✔
572
                Str("op", "Read").
205✔
573
                Uint64("nodeID", in.NodeId).
205✔
574
                Str("id", id).
205✔
575
                Str("path", path).
205✔
576
                Int("bufsize", len(buf)).
205✔
577
                Logger()
205✔
578
        ctx.Trace().Msg("")
205✔
579

205✔
580
        fd, err := f.content.Open(id)
205✔
581
        if err != nil {
205✔
582
                ctx.Error().Err(err).Msg("Cache Open() failed.")
×
583
                return fuse.ReadResultData(make([]byte, 0)), fuse.EIO
×
584
        }
×
585

586
        // we are locked for the remainder of this op
587
        inode.RLock()
205✔
588
        defer inode.RUnlock()
205✔
589
        return fuse.ReadResultFd(fd.Fd(), int64(in.Offset), int(in.Size)), fuse.OK
205✔
590
}
591

592
// Write to an Inode like a file. Note that changes are 100% local until
593
// Flush() is called. Returns the number of bytes written and the status of the
594
// op.
595
func (f *Filesystem) Write(cancel <-chan struct{}, in *fuse.WriteIn, data []byte) (uint32, fuse.Status) {
824✔
596
        id := f.TranslateID(in.NodeId)
824✔
597
        inode := f.GetID(id)
824✔
598
        if inode == nil {
824✔
599
                return 0, fuse.EBADF
×
600
        }
×
601

602
        nWrite := len(data)
824✔
603
        offset := int(in.Offset)
824✔
604
        ctx := log.With().
824✔
605
                Str("op", "Write").
824✔
606
                Str("id", id).
824✔
607
                Uint64("nodeID", in.NodeId).
824✔
608
                Str("path", inode.Path()).
824✔
609
                Int("bufsize", nWrite).
824✔
610
                Int("offset", offset).
824✔
611
                Logger()
824✔
612
        ctx.Trace().Msg("")
824✔
613

824✔
614
        fd, err := f.content.Open(id)
824✔
615
        if err != nil {
824✔
616
                ctx.Error().Msg("Cache Open() failed.")
×
617
                return 0, fuse.EIO
×
618
        }
×
619

620
        inode.Lock()
824✔
621
        defer inode.Unlock()
824✔
622
        n, err := fd.WriteAt(data, int64(offset))
824✔
623
        if err != nil {
824✔
624
                ctx.Error().Err(err).Msg("Error during write")
×
625
                return uint32(n), fuse.EIO
×
626
        }
×
627

628
        st, _ := fd.Stat()
824✔
629
        inode.DriveItem.Size = uint64(st.Size())
824✔
630
        inode.hasChanges = true
824✔
631
        return uint32(n), fuse.OK
824✔
632
}
633

634
// Fsync is a signal to ensure writes to the Inode are flushed to stable
635
// storage. This method is used to trigger uploads of file content.
636
func (f *Filesystem) Fsync(cancel <-chan struct{}, in *fuse.FsyncIn) fuse.Status {
82✔
637
        id := f.TranslateID(in.NodeId)
82✔
638
        inode := f.GetID(id)
82✔
639
        if inode == nil {
82✔
640
                return fuse.EBADF
×
641
        }
×
642

643
        ctx := log.With().
82✔
644
                Str("op", "Fsync").
82✔
645
                Str("id", id).
82✔
646
                Uint64("nodeID", in.NodeId).
82✔
647
                Str("path", inode.Path()).
82✔
648
                Logger()
82✔
649
        ctx.Debug().Msg("")
82✔
650
        if inode.HasChanges() {
130✔
651
                inode.Lock()
48✔
652
                inode.hasChanges = false
48✔
653

48✔
654
                // recompute hashes when saving new content
48✔
655
                inode.DriveItem.File = &graph.File{}
48✔
656
                fd, err := f.content.Open(id)
48✔
657
                if err != nil {
48✔
658
                        ctx.Error().Err(err).Msg("Could not get fd.")
×
659
                }
×
660
                fd.Sync()
48✔
661
                inode.DriveItem.File.Hashes.QuickXorHash = graph.QuickXORHashStream(fd)
48✔
662
                inode.Unlock()
48✔
663

48✔
664
                if err := f.uploads.QueueUpload(inode); err != nil {
48✔
665
                        ctx.Error().Err(err).Msg("Error creating upload session.")
×
666
                        return fuse.EREMOTEIO
×
667
                }
×
668
                return fuse.OK
48✔
669
        }
670
        return fuse.OK
34✔
671
}
672

673
// Flush is called when a file descriptor is closed. Uses Fsync() to perform file
674
// uploads. (Release not implemented because all cleanup is already done here).
675
func (f *Filesystem) Flush(cancel <-chan struct{}, in *fuse.FlushIn) fuse.Status {
81✔
676
        inode := f.GetNodeID(in.NodeId)
81✔
677
        if inode == nil {
81✔
678
                return fuse.EBADF
×
679
        }
×
680

681
        id := inode.ID()
81✔
682
        log.Trace().
81✔
683
                Str("op", "Flush").
81✔
684
                Str("id", id).
81✔
685
                Str("path", inode.Path()).
81✔
686
                Uint64("nodeID", in.NodeId).
81✔
687
                Msg("")
81✔
688
        f.Fsync(cancel, &fuse.FsyncIn{InHeader: in.InHeader})
81✔
689
        f.content.Close(id)
81✔
690
        return 0
81✔
691
}
692

693
// Getattr returns a the Inode as a UNIX stat. Holds the read mutex for all of
694
// the "metadata fetch" operations.
695
func (f *Filesystem) GetAttr(cancel <-chan struct{}, in *fuse.GetAttrIn, out *fuse.AttrOut) fuse.Status {
76✔
696
        id := f.TranslateID(in.NodeId)
76✔
697
        inode := f.GetID(id)
76✔
698
        if inode == nil {
76✔
699
                return fuse.ENOENT
×
700
        }
×
701
        log.Trace().
76✔
702
                Str("op", "GetAttr").
76✔
703
                Uint64("nodeID", in.NodeId).
76✔
704
                Str("id", id).
76✔
705
                Str("path", inode.Path()).
76✔
706
                Msg("")
76✔
707

76✔
708
        out.Attr = inode.makeAttr()
76✔
709
        out.SetTimeout(timeout)
76✔
710
        return fuse.OK
76✔
711
}
712

713
// Setattr is the workhorse for setting filesystem attributes. Does the work of
714
// operations like utimens, chmod, chown (not implemented, FUSE is single-user),
715
// and truncate.
716
func (f *Filesystem) SetAttr(cancel <-chan struct{}, in *fuse.SetAttrIn, out *fuse.AttrOut) fuse.Status {
20✔
717
        i := f.GetNodeID(in.NodeId)
20✔
718
        if i == nil {
20✔
719
                return fuse.ENOENT
×
720
        }
×
721
        path := i.Path()
20✔
722
        isDir := i.IsDir() // holds an rlock
20✔
723
        i.Lock()
20✔
724

20✔
725
        ctx := log.With().
20✔
726
                Str("op", "SetAttr").
20✔
727
                Uint64("nodeID", in.NodeId).
20✔
728
                Str("id", i.DriveItem.ID).
20✔
729
                Str("path", path).
20✔
730
                Logger()
20✔
731

20✔
732
        // utimens
20✔
733
        if mtime, valid := in.GetMTime(); valid {
25✔
734
                ctx.Info().
5✔
735
                        Str("subop", "utimens").
5✔
736
                        Time("oldMtime", *i.DriveItem.ModTime).
5✔
737
                        Time("newMtime", *i.DriveItem.ModTime).
5✔
738
                        Msg("")
5✔
739
                i.DriveItem.ModTime = &mtime
5✔
740
        }
5✔
741

742
        // chmod
743
        if mode, valid := in.GetMode(); valid {
23✔
744
                ctx.Info().
3✔
745
                        Str("subop", "chmod").
3✔
746
                        Str("oldMode", Octal(i.mode)).
3✔
747
                        Str("newMode", Octal(mode)).
3✔
748
                        Msg("")
3✔
749
                if isDir {
3✔
750
                        i.mode = fuse.S_IFDIR | mode
×
751
                } else {
3✔
752
                        i.mode = fuse.S_IFREG | mode
3✔
753
                }
3✔
754
        }
755

756
        // truncate
757
        if size, valid := in.GetSize(); valid {
31✔
758
                ctx.Info().
11✔
759
                        Str("subop", "truncate").
11✔
760
                        Uint64("oldSize", i.DriveItem.Size).
11✔
761
                        Uint64("newSize", size).
11✔
762
                        Msg("")
11✔
763
                fd, _ := f.content.Open(i.DriveItem.ID)
11✔
764
                fd.Truncate(int64(size))
11✔
765
                i.DriveItem.Size = size
11✔
766
                i.hasChanges = true
11✔
767
        }
11✔
768

769
        i.Unlock()
20✔
770
        out.Attr = i.makeAttr()
20✔
771
        out.SetTimeout(timeout)
20✔
772
        return fuse.OK
20✔
773
}
774

775
// Rename renames and/or moves an inode.
776
func (f *Filesystem) Rename(cancel <-chan struct{}, in *fuse.RenameIn, name string, newName string) fuse.Status {
8✔
777
        if isNameRestricted(newName) {
9✔
778
                return fuse.EINVAL
1✔
779
        }
1✔
780

781
        oldParentID := f.TranslateID(in.NodeId)
7✔
782
        oldParentItem := f.GetNodeID(in.NodeId)
7✔
783
        if oldParentID == "" || oldParentItem == nil {
7✔
784
                return fuse.EBADF
×
785
        }
×
786
        path := filepath.Join(oldParentItem.Path(), name)
7✔
787

7✔
788
        // we'll have the metadata for the dest inode already so it is not necessary
7✔
789
        // to use GetPath() to prefetch it. In order for the fs to know about this
7✔
790
        // inode, it has already fetched all of the inodes up to the new destination.
7✔
791
        newParentItem := f.GetNodeID(in.Newdir)
7✔
792
        if newParentItem == nil {
7✔
793
                return fuse.ENOENT
×
794
        }
×
795
        dest := filepath.Join(newParentItem.Path(), newName)
7✔
796

7✔
797
        inode, _ := f.GetChild(oldParentID, name, f.auth)
7✔
798
        id, err := f.remoteID(inode)
7✔
799
        newParentID := newParentItem.ID()
7✔
800

7✔
801
        ctx := log.With().
7✔
802
                Str("op", "Rename").
7✔
803
                Str("id", id).
7✔
804
                Str("parentID", newParentID).
7✔
805
                Str("path", path).
7✔
806
                Str("dest", dest).
7✔
807
                Logger()
7✔
808
        ctx.Info().
7✔
809
                Uint64("srcNodeID", in.NodeId).
7✔
810
                Uint64("dstNodeID", in.Newdir).
7✔
811
                Msg("")
7✔
812

7✔
813
        if isLocalID(id) || err != nil {
7✔
814
                // uploads will fail without an id
×
815
                ctx.Error().Err(err).
×
816
                        Msg("ID of item to move cannot be local and we failed to obtain an ID.")
×
817
                return fuse.EREMOTEIO
×
818
        }
×
819

820
        // perform remote rename
821
        if err = graph.Rename(id, newName, newParentID, f.auth); err != nil {
7✔
822
                ctx.Error().Err(err).Msg("Failed to rename remote item.")
×
823
                return fuse.EREMOTEIO
×
824
        }
×
825

826
        // now rename local copy
827
        if err = f.MovePath(oldParentID, newParentID, name, newName, f.auth); err != nil {
7✔
828
                ctx.Error().Err(err).Msg("Failed to rename local item.")
×
829
                return fuse.EIO
×
830
        }
×
831

832
        // whew! item renamed
833
        return fuse.OK
7✔
834
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc