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

jstaf / onedriver / 6554536784

18 Oct 2023 12:37AM UTC coverage: 74.287% (+0.5%) from 73.744%
6554536784

Pull #359

github

jstaf
fix hash stream functions
Pull Request #359: Fix file redownloads, Ubuntu 20/Debian 11 compatibility

13 of 17 new or added lines in 3 files covered. (76.47%)

13 existing lines in 3 files now uncovered.

1901 of 2559 relevant lines covered (74.29%)

341244.46 hits per line

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

76.11
/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 {
108✔
20
        i.RLock()
108✔
21
        defer i.RUnlock()
108✔
22
        data := f.content.Get(i.DriveItem.ID)
108✔
23
        return &data
108✔
24
}
108✔
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) {
14✔
30
        if i.IsDir() {
14✔
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()
14✔
37
        if isLocalID(originalID) && f.auth.AccessToken != "" {
26✔
38
                // perform a blocking upload of the item
12✔
39
                data := f.getInodeContent(i)
12✔
40
                session, err := NewUploadSession(i, data)
12✔
41
                if err != nil {
12✔
42
                        return originalID, err
×
43
                }
×
44

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

1✔
51
                        if strings.Contains(err.Error(), "nameAlreadyExists") {
2✔
52
                                // A file with this name already exists on the server, get its ID and
1✔
53
                                // use that. This is probably the same file, but just got uploaded
1✔
54
                                // earlier.
1✔
55
                                children, err := graph.GetItemChildren(i.ParentID(), f.auth)
1✔
56
                                if err != nil {
1✔
57
                                        return originalID, err
×
58
                                }
×
59
                                for _, child := range children {
7✔
60
                                        if child.Name == name {
7✔
61
                                                log.Info().
1✔
62
                                                        Str("name", name).
1✔
63
                                                        Str("originalID", originalID).
1✔
64
                                                        Str("newID", child.ID).
1✔
65
                                                        Msg("Exchanged ID.")
1✔
66
                                                return child.ID, f.MoveID(originalID, child.ID)
1✔
67
                                        }
1✔
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
11✔
76
                i.DriveItem.ETag = session.ETag
11✔
77
                i.Unlock()
11✔
78

11✔
79
                // this is all we really wanted from this transaction
11✔
80
                err = f.MoveID(originalID, session.ID)
11✔
81
                log.Info().
11✔
82
                        Str("name", name).
11✔
83
                        Str("originalID", originalID).
11✔
84
                        Str("newID", session.ID).
11✔
85
                        Msg("Exchanged ID.")
11✔
86
                return session.ID, err
11✔
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 {
142✔
96
        if strings.EqualFold(name, "CON") {
142✔
97
                return true
×
98
        }
×
99
        if strings.EqualFold(name, "AUX") {
142✔
100
                return true
×
101
        }
×
102
        if strings.EqualFold(name, "PRN") {
142✔
103
                return true
×
104
        }
×
105
        if strings.EqualFold(name, "NUL") {
142✔
106
                return true
×
107
        }
×
108
        if strings.EqualFold(name, ".lock") {
142✔
109
                return true
×
110
        }
×
111
        if strings.EqualFold(name, "desktop.ini") {
144✔
112
                return true
2✔
113
        }
2✔
114
        return disallowedRexp.FindStringIndex(name) != nil
140✔
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 {
4✔
120
        ctx := log.With().Str("op", "StatFs").Logger()
4✔
121
        ctx.Debug().Msg("")
4✔
122
        drive, err := graph.GetDrive(f.auth)
4✔
123
        if err != nil {
4✔
124
                return fuse.EREMOTEIO
×
125
        }
×
126

127
        if drive.DriveType == graph.DriveTypePersonal {
6✔
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
4✔
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
4✔
140
        out.Bsize = uint32(blkSize)
4✔
141
        out.Blocks = drive.Quota.Total / blkSize
4✔
142
        out.Bfree = drive.Quota.Remaining / blkSize
4✔
143
        out.Bavail = drive.Quota.Remaining / blkSize
4✔
144
        out.Files = 100000
4✔
145
        out.Ffree = 100000 - drive.Quota.FileCount
4✔
146
        out.NameLen = 260
4✔
147
        return fuse.OK
4✔
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 {
34✔
152
        if isNameRestricted(name) {
42✔
153
                return fuse.EINVAL
8✔
154
        }
8✔
155

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

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

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

24✔
181
        out.NodeId = f.InsertChild(id, newInode)
24✔
182
        out.Attr = newInode.makeAttr()
24✔
183
        out.SetAttrTimeout(timeout)
24✔
184
        out.SetEntryTimeout(timeout)
24✔
185
        return fuse.OK
24✔
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 {
14✔
190
        parentID := f.TranslateID(in.NodeId)
14✔
191
        if parentID == "" {
14✔
192
                return fuse.ENOENT
×
193
        }
×
194
        child, _ := f.GetChild(parentID, name, f.auth)
14✔
195
        if child == nil {
14✔
196
                return fuse.ENOENT
×
197
        }
×
198
        if child.HasChildren() {
18✔
199
                return fuse.Status(syscall.ENOTEMPTY)
4✔
200
        }
4✔
201
        return f.Unlink(cancel, in, name)
10✔
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 {
26✔
206
        id := f.TranslateID(in.NodeId)
26✔
207
        dir := f.GetID(id)
26✔
208
        if dir == nil {
26✔
209
                return fuse.ENOENT
×
210
        }
×
211
        if !dir.IsDir() {
26✔
212
                return fuse.ENOTDIR
×
213
        }
×
214
        path := dir.Path()
26✔
215
        ctx := log.With().
26✔
216
                Str("op", "OpenDir").
26✔
217
                Uint64("nodeID", in.NodeId).
26✔
218
                Str("id", id).
26✔
219
                Str("path", path).Logger()
26✔
220
        ctx.Debug().Msg("")
26✔
221

26✔
222
        children, err := f.GetChildrenID(id, f.auth)
26✔
223
        if err != nil {
26✔
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())
26✔
231
        if parent == nil {
34✔
232
                // This is the parent of the mountpoint. The FUSE kernel module discards
8✔
233
                // this info, so what we put here doesn't actually matter.
8✔
234
                parent = NewInode("..", 0755|fuse.S_IFDIR, nil)
8✔
235
                parent.nodeID = math.MaxUint64
8✔
236
        }
8✔
237

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

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

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

252
// ReleaseDir closes a directory and purges it from memory
253
func (f *Filesystem) ReleaseDir(in *fuse.ReleaseIn) {
26✔
254
        f.opendirsM.Lock()
26✔
255
        delete(f.opendirs, in.NodeId)
26✔
256
        f.opendirsM.Unlock()
26✔
257
}
26✔
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 {
809✔
261
        f.opendirsM.RLock()
809✔
262
        entries, ok := f.opendirs[in.NodeId]
809✔
263
        f.opendirsM.RUnlock()
809✔
264
        if !ok {
809✔
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)) {
831✔
276
                // just tried to seek past end of directory, we're all done!
22✔
277
                return fuse.OK
22✔
278
        }
22✔
279

280
        inode := entries[in.Offset]
787✔
281
        entry := fuse.DirEntry{
787✔
282
                Ino:  inode.NodeID(),
787✔
283
                Mode: inode.Mode(),
787✔
284
        }
787✔
285
        // first two entries will always be "." and ".."
787✔
286
        switch in.Offset {
787✔
287
        case 0:
22✔
288
                entry.Name = "."
22✔
289
        case 1:
22✔
290
                entry.Name = ".."
22✔
291
        default:
743✔
292
                entry.Name = inode.Name()
743✔
293
        }
294
        entryOut := out.AddDirLookupEntry(entry)
787✔
295
        if entryOut == nil {
787✔
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
787✔
307
        entryOut.Attr = inode.makeAttr()
787✔
308
        entryOut.SetAttrTimeout(timeout)
787✔
309
        entryOut.SetEntryTimeout(timeout)
787✔
310
        return fuse.OK
787✔
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 {
699✔
357
        id := f.TranslateID(in.NodeId)
699✔
358
        log.Trace().
699✔
359
                Str("op", "Lookup").
699✔
360
                Uint64("nodeID", in.NodeId).
699✔
361
                Str("id", id).
699✔
362
                Str("name", name).
699✔
363
                Msg("")
699✔
364

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

370
        out.NodeId = child.NodeID()
506✔
371
        out.Attr = child.makeAttr()
506✔
372
        out.SetAttrTimeout(timeout)
506✔
373
        out.SetEntryTimeout(timeout)
506✔
374
        return fuse.OK
506✔
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 {
92✔
379
        if isNameRestricted(name) {
100✔
380
                return fuse.EINVAL
8✔
381
        }
8✔
382

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

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

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

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

408
        inode := NewInode(name, in.Mode, parent)
80✔
409
        ctx.Debug().
80✔
410
                Str("childID", inode.ID()).
80✔
411
                Str("mode", Octal(in.Mode)).
80✔
412
                Msg("Creating inode.")
80✔
413
        out.NodeId = f.InsertChild(parentID, inode)
80✔
414
        out.Attr = inode.makeAttr()
80✔
415
        out.SetAttrTimeout(timeout)
80✔
416
        out.SetEntryTimeout(timeout)
80✔
417
        return fuse.OK
80✔
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 {
92✔
422
        // we reuse mknod here
92✔
423
        result := f.Mknod(
92✔
424
                cancel,
92✔
425
                // we don't actually use the umask or padding here, so they don't get passed
92✔
426
                &fuse.MknodIn{
92✔
427
                        InHeader: in.InHeader,
92✔
428
                        Mode:     in.Mode,
92✔
429
                },
92✔
430
                name,
92✔
431
                &out.EntryOut,
92✔
432
        )
92✔
433
        if result == fuse.Status(syscall.EEXIST) {
94✔
434
                // if the inode already exists, we should truncate the existing file and
2✔
435
                // return the existing file inode as per "man creat"
2✔
436
                parentID := f.TranslateID(in.NodeId)
2✔
437
                child, _ := f.GetChild(parentID, name, f.auth)
2✔
438
                log.Debug().
2✔
439
                        Str("op", "Create").
2✔
440
                        Uint64("nodeID", in.NodeId).
2✔
441
                        Str("id", parentID).
2✔
442
                        Str("childID", child.ID()).
2✔
443
                        Str("path", child.Path()).
2✔
444
                        Str("mode", Octal(in.Mode)).
2✔
445
                        Msg("Child inode already exists, truncating.")
2✔
446
                f.content.Delete(child.ID())
2✔
447
                f.content.Open(child.ID())
2✔
448
                child.DriveItem.Size = 0
2✔
449
                child.hasChanges = true
2✔
450
                return fuse.OK
2✔
451
        }
2✔
452
        // no further initialized required to open the file, it's empty
453
        return result
90✔
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 {
74✔
459
        id := f.TranslateID(in.NodeId)
74✔
460
        inode := f.GetID(id)
74✔
461
        if inode == nil {
74✔
462
                return fuse.ENOENT
×
463
        }
×
464

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

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

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

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

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

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

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

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

UNCOV
513
        ctx.Info().Msg(
×
UNCOV
514
                "Not using cached item due to file hash mismatch, fetching content from API.",
×
UNCOV
515
        )
×
NEW
516
        // explicitly purge existing file content
×
NEW
517
        fd.Seek(0, 0)
×
NEW
518
        fd.Truncate(0)
×
UNCOV
519
        size, err := graph.GetItemContentStream(id, f.auth, fd)
×
NEW
520
        if err != nil || !inode.VerifyChecksum(graph.QuickXORHashStream(fd)) {
×
521
                ctx.Error().Err(err).Msg("Failed to fetch remote content.")
×
522
                return fuse.EREMOTEIO
×
523
        }
×
UNCOV
524
        inode.DriveItem.Size = size
×
UNCOV
525
        return fuse.OK
×
526
}
527

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

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

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

560
        f.DeleteID(id)
14✔
561
        f.content.Delete(id)
14✔
562
        return fuse.OK
14✔
563
}
564

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

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

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

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

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

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

1,648✔
617
        fd, err := f.content.Open(id)
1,648✔
618
        if err != nil {
1,648✔
619
                ctx.Error().Msg("Cache Open() failed.")
×
620
                return 0, fuse.EIO
×
621
        }
×
622

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

631
        st, _ := fd.Stat()
1,648✔
632
        inode.DriveItem.Size = uint64(st.Size())
1,648✔
633
        inode.hasChanges = true
1,648✔
634
        return uint32(n), fuse.OK
1,648✔
635
}
636

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

646
        ctx := log.With().
168✔
647
                Str("op", "Fsync").
168✔
648
                Str("id", id).
168✔
649
                Uint64("nodeID", in.NodeId).
168✔
650
                Str("path", inode.Path()).
168✔
651
                Logger()
168✔
652
        ctx.Debug().Msg("")
168✔
653
        if inode.HasChanges() {
264✔
654
                inode.Lock()
96✔
655
                inode.hasChanges = false
96✔
656

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

96✔
667
                if err := f.uploads.QueueUpload(inode); err != nil {
96✔
668
                        ctx.Error().Err(err).Msg("Error creating upload session.")
×
669
                        return fuse.EREMOTEIO
×
670
                }
×
671
                return fuse.OK
96✔
672
        }
673
        return fuse.OK
72✔
674
}
675

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

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

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

151✔
711
        out.Attr = inode.makeAttr()
151✔
712
        out.SetTimeout(timeout)
151✔
713
        return fuse.OK
151✔
714
}
715

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

40✔
728
        ctx := log.With().
40✔
729
                Str("op", "SetAttr").
40✔
730
                Uint64("nodeID", in.NodeId).
40✔
731
                Str("id", i.DriveItem.ID).
40✔
732
                Str("path", path).
40✔
733
                Logger()
40✔
734

40✔
735
        // utimens
40✔
736
        if mtime, valid := in.GetMTime(); valid {
50✔
737
                ctx.Info().
10✔
738
                        Str("subop", "utimens").
10✔
739
                        Time("oldMtime", *i.DriveItem.ModTime).
10✔
740
                        Time("newMtime", *i.DriveItem.ModTime).
10✔
741
                        Msg("")
10✔
742
                i.DriveItem.ModTime = &mtime
10✔
743
        }
10✔
744

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

759
        // truncate
760
        if size, valid := in.GetSize(); valid {
62✔
761
                ctx.Info().
22✔
762
                        Str("subop", "truncate").
22✔
763
                        Uint64("oldSize", i.DriveItem.Size).
22✔
764
                        Uint64("newSize", size).
22✔
765
                        Msg("")
22✔
766
                fd, _ := f.content.Open(i.DriveItem.ID)
22✔
767
                // the unix syscall does not update the seek position, so neither should we
22✔
768
                fd.Truncate(int64(size))
22✔
769
                i.DriveItem.Size = size
22✔
770
                i.hasChanges = true
22✔
771
        }
22✔
772

773
        i.Unlock()
40✔
774
        out.Attr = i.makeAttr()
40✔
775
        out.SetTimeout(timeout)
40✔
776
        return fuse.OK
40✔
777
}
778

779
// Rename renames and/or moves an inode.
780
func (f *Filesystem) Rename(cancel <-chan struct{}, in *fuse.RenameIn, name string, newName string) fuse.Status {
16✔
781
        if isNameRestricted(newName) {
18✔
782
                return fuse.EINVAL
2✔
783
        }
2✔
784

785
        oldParentID := f.TranslateID(in.NodeId)
14✔
786
        oldParentItem := f.GetNodeID(in.NodeId)
14✔
787
        if oldParentID == "" || oldParentItem == nil {
14✔
788
                return fuse.EBADF
×
789
        }
×
790
        path := filepath.Join(oldParentItem.Path(), name)
14✔
791

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

14✔
801
        inode, _ := f.GetChild(oldParentID, name, f.auth)
14✔
802
        id, err := f.remoteID(inode)
14✔
803
        newParentID := newParentItem.ID()
14✔
804

14✔
805
        ctx := log.With().
14✔
806
                Str("op", "Rename").
14✔
807
                Str("id", id).
14✔
808
                Str("parentID", newParentID).
14✔
809
                Str("path", path).
14✔
810
                Str("dest", dest).
14✔
811
                Logger()
14✔
812
        ctx.Info().
14✔
813
                Uint64("srcNodeID", in.NodeId).
14✔
814
                Uint64("dstNodeID", in.Newdir).
14✔
815
                Msg("")
14✔
816

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

824
        // perform remote rename
825
        if err = graph.Rename(id, newName, newParentID, f.auth); err != nil {
14✔
826
                ctx.Error().Err(err).Msg("Failed to rename remote item.")
×
827
                return fuse.EREMOTEIO
×
828
        }
×
829

830
        // now rename local copy
831
        if err = f.MovePath(oldParentID, newParentID, name, newName, f.auth); err != nil {
14✔
832
                ctx.Error().Err(err).Msg("Failed to rename local item.")
×
833
                return fuse.EIO
×
834
        }
×
835

836
        // whew! item renamed
837
        return fuse.OK
14✔
838
}
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