Skip to content

Commit

Permalink
implement basic read-only fuse drive
Browse files Browse the repository at this point in the history
  • Loading branch information
seborama committed Apr 22, 2024
1 parent 4c37e35 commit eea98cb
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 60 deletions.
10 changes: 9 additions & 1 deletion fuse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ I am developing on a Linux ARM Raspberry Pi4. I haven't (yet) tried Linux x86_64

## Status

At this stage, this is purely explorative. The code base is entirely experimental, most features are not implemented or only partially.
At this stage, this is explorative. The code base is entirely experimental, most features are not implemented or only partially.

The drive can be mounted via the tests and it can be "walked" through.

Files contents cannot be read just yet.

No write operations are supported

## Change log

2024-Apr-22 - The pCloud drive can listed entirely. `ls` on the root of the mount will list directories and files contained in the root of the pCloud drive.

2024-Apr-21 - The pCloud drive can be mounted (via the test - see "Getting started"). `ls` on the root of the mount will list directories and files contained in the root of the pCloud drive.

## Getting started
Expand Down
184 changes: 137 additions & 47 deletions fuse/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"context"
"log"
"os"
"os/user"
"strconv"
"syscall"
"time"

"bazil.org/fuse"
"bazil.org/fuse/fs"
Expand All @@ -18,19 +21,37 @@ type Drive struct {
conn *fuse.Conn // TODO: define an interface
}

func Mount(mountpoint string, pcClient *sdk.Client) (Drive, error) {
func Mount(mountpoint string, pcClient *sdk.Client) (*Drive, error) {
conn, err := fuse.Mount(
mountpoint,
fuse.FSName("pcloud"),
fuse.Subtype("seborama"),
)
if err != nil {
log.Fatal(err)
return nil, err
}

return Drive{
user, err := user.Current()
if err != nil {
return nil, err
}
uid, err := strconv.ParseUint(user.Uid, 10, 32)
if err != nil {
return nil, err
}
gid, err := strconv.ParseUint(user.Gid, 10, 32)
if err != nil {
return nil, err
}

return &Drive{
fs: &FS{
pcClient: pcClient,
pcClient: pcClient,
uid: uint32(uid),
gid: uint32(gid),
rdev: 531,
dirPerms: 0o750,
filePerms: 0o640,
},
conn: conn,
}, nil
Expand All @@ -46,7 +67,12 @@ func (d *Drive) Serve() error {

// FS implements the pCloud file system.
type FS struct {
pcClient *sdk.Client // TODO: define an interface
pcClient *sdk.Client // TODO: define an interface
uid uint32
gid uint32
rdev uint32
dirPerms os.FileMode
filePerms os.FileMode
}

// ensure interfaces conpliance
Expand All @@ -56,110 +82,174 @@ var (

func (fs *FS) Root() (fs.Node, error) {
log.Println("Root called")
fsList, err := fs.pcClient.ListFolder(context.Background(), sdk.T1FolderByID(sdk.RootFolderID), false, false, false, false)

rootDir := &Dir{
Type: fuse.DT_Dir,
fs: fs,
}

err := rootDir.materialiseFolder(context.Background())
if err != nil {
return nil, err
}

return rootDir, nil
}

// Dir implements both Node and Handle for the root directory.
type Dir struct {
Type fuse.DirentType
Attributes fuse.Attr

// TODO: we must be able to find something better than interface{}, either a proper interface or perhaps a generic type
// TODO: we likely don't need this: we should always call `materialiseFolder()` because the source of truth is pCloud
// TODO: contents is subject to changes at anytime, and we should allow the fuse driver to be the judge of whether to
// TODO: ... refresh the folder or not via fuse.Attr.Validate
Entries map[string]interface{}

fs *FS
parentFolderID uint64
folderID uint64
}

// ensure interfaces conpliance
var (
_ fs.Node = (*Dir)(nil)
_ fs.NodeStringLookuper = (*Dir)(nil)
_ fs.HandleReadDirAller = (*Dir)(nil)
)

func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error {
log.Println("Dir.Attr called")
*a = d.Attributes
return nil
}

// TODO: add support for . and ..
func (d *Dir) materialiseFolder(ctx context.Context) error {
fsList, err := d.fs.pcClient.ListFolder(ctx, sdk.T1FolderByID(d.folderID), false, false, false, false)
if err != nil {
return err
}

// TODO: is this necessary? perhaps only for the root folder?
d.Attributes = fuse.Attr{
Valid: time.Second,
Inode: d.folderID,
Atime: fsList.Metadata.Modified.Time,
Mtime: fsList.Metadata.Modified.Time,
Ctime: fsList.Metadata.Modified.Time,
Mode: os.ModeDir | d.fs.dirPerms,
Nlink: 1, // TODO: is that right? How else can we find this value?
Uid: d.fs.uid,
Gid: d.fs.gid,
Rdev: d.fs.rdev,
}

d.parentFolderID = fsList.Metadata.ParentFolderID
d.folderID = fsList.Metadata.FolderID

entries := lo.SliceToMap(fsList.Metadata.Contents, func(item *sdk.Metadata) (string, interface{}) {
if item.IsFolder {
return item.Name, &Dir{
Type: fuse.DT_Dir,
Attributes: fuse.Attr{
Valid: time.Second,
Inode: item.FolderID,
Atime: item.Modified.Time,
Mtime: item.Modified.Time,
Ctime: item.Modified.Time,
Mode: os.ModeDir | 0o777,
Mode: os.ModeDir | d.fs.dirPerms,
Nlink: 1, // the official pCloud client can show other values that 1 - dunno how
Uid: d.fs.uid,
Gid: d.fs.gid,
Rdev: d.fs.rdev,
},
Entries: map[string]interface{}{}, // TODO
Entries: nil, // will be populated by Dir.Lookup
fs: d.fs,
parentFolderID: item.ParentFolderID,
folderID: item.FolderID,
}
}

return item.Name, &File{
Type: fuse.DT_File,
// Content: content, // TODO
Attributes: fuse.Attr{
Valid: time.Second,
Inode: item.FileID,
Size: item.Size,
Atime: item.Modified.Time,
Mtime: item.Modified.Time,
Ctime: item.Modified.Time,
Mode: 0o777,
Mode: d.fs.filePerms,
Nlink: 1, // TODO: is that right? How else can we find this value?
Uid: d.fs.uid,
Gid: d.fs.gid,
Rdev: d.fs.rdev,
},
}
})

rootDir := &Dir{
Type: fuse.DT_Dir,
Attributes: fuse.Attr{
Inode: sdk.RootFolderID,
Atime: fsList.Metadata.Modified.Time,
Mtime: fsList.Metadata.Modified.Time,
Ctime: fsList.Metadata.Modified.Time,
Mode: os.ModeDir | 0o777,
},
Entries: entries,
}

return rootDir, nil
}

// Dir implements both Node and Handle for the root directory.
type Dir struct {
Type fuse.DirentType
Attributes fuse.Attr
Entries map[string]interface{}
}

// ensure interfaces conpliance
var (
_ fs.Node = (*Dir)(nil)
_ fs.NodeStringLookuper = (*Dir)(nil)
)
d.Entries = entries

func (d Dir) Attr(ctx context.Context, a *fuse.Attr) error {
log.Println("Dir.Attr called")
log.Println("File.Attr called")
*a = d.Attributes
return nil
}

func (d Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
log.Println("Dir.Lookup called")
// Lookup looks up a specific entry in the receiver,
// which must be a directory. Lookup should return a Node
// corresponding to the entry. If the name does not exist in
// the directory, Lookup should return ENOENT.
//
// Lookup need not to handle the names "." and "..".
func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
log.Println("Dir.Lookup called on dir folderID:", d.folderID, "entries count:", len(d.Entries), "- with name:", name)
// TODO: this test is likely incorrect: we should always list entries in case the folder has changed
// TODO: ...at the very least, we should combine it with a TTL or simply rely on the fuse driver to manage that for us.
if len(d.Entries) == 0 {
// TODO: we can do better here: all this function wants is to get a single entry, not everything
d.materialiseFolder(ctx)
}

node, ok := d.Entries[name]
if ok {
return node.(fs.Node), nil
}

return nil, syscall.ENOENT
}

func (d Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
log.Println("Dir.ReadDirAll called")
entries := lo.MapToSlice(d.Entries, func(key string, value interface{}) fuse.Dirent {
func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
log.Println("Dir.ReadDirAll called - folderID:", d.folderID, "-", "parentFolderID:", d.parentFolderID)
d.materialiseFolder(ctx) // TODO: this should not be required here

dirEntries := lo.MapToSlice(d.Entries, func(key string, value interface{}) fuse.Dirent {
switch castEntry := value.(type) {
case *File:
return fuse.Dirent{
Inode: castEntry.Attributes.Inode,
Type: castEntry.Type,
Name: key,
}

case *Dir:
return fuse.Dirent{
Inode: castEntry.Attributes.Inode,
Type: castEntry.Type,
Name: key,
}

default:
log.Printf("unknown directory entry '%T'", castEntry)
log.Printf("unknown directory entry type '%T'", castEntry)
return fuse.Dirent{
Inode: 6_666_666_666_666_666_666,
Type: fuse.DT_Unknown,
Name: key,
}
}
})
return entries, nil

return dirEntries, nil
}

// File implements both Node and Handle for the hello file.
Expand Down
6 changes: 3 additions & 3 deletions fuse/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ func Test(t *testing.T) {

mountpoint := "/tmp/pcloud_mnt"

c, err := pfuse.Mount(
drive, err := pfuse.Mount(
mountpoint,
pcClient,
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = c.Unmount() }()
defer func() { _ = drive.Unmount() }()

log.Println("Mouting FS")
err = c.Serve()
err = drive.Serve()
if err != nil {
log.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ TFA was possible thanks to [Glib Dzevo](https://github.com/gdzevo) and his [cons
- ✅ checksumfile
- ✅ deletefile
- ✅ renamefile
- stat
- stat
- Auth
- sendverificationemail
- verifyemail
Expand Down
17 changes: 17 additions & 0 deletions sdk/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ func (c *Client) RenameFile(ctx context.Context, file T3PathOrFileID, destinatio
return r, nil
}

// Stat returns information about the file pointed to by fileid or path.
// It's is recomended to use fileid.
// https://docs.pcloud.com/methods/file/stat.html
func (c *Client) Stat(ctx context.Context, file T3PathOrFileID, opts ...ClientOption) (*FileResult, error) {
q := toQuery(opts...)
file(q)

r := &FileResult{}

err := parseAPIOutput(r)(c.get(ctx, "stat", q))
if err != nil {
return nil, err
}

return r, nil
}

// CopyFile takes one file and copies it as another file in the user's filesystem.
// Expects fileid or path to identify the source file and tofolderid+toname or topath to
// identify destination filename.
Expand Down
7 changes: 7 additions & 0 deletions sdk/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ func (testsuite *IntegrationTestSuite) Test_UploadFile() {
}
}

func (testsuite *IntegrationTestSuite) Test_Stat() {
fs, err := testsuite.pcc.Stat(testsuite.ctx, sdk.T3FileByID(testsuite.testFileID))
testsuite.Require().NoError(err)
testsuite.Equal(testsuite.testFileID, fs.Metadata.FileID)
testsuite.Equal("sample.file", fs.Metadata.Name)
}

func (testsuite *IntegrationTestSuite) createFiles() map[string]*os.File {
num := 3
files := map[string]*os.File{}
Expand Down
Loading

0 comments on commit eea98cb

Please sign in to comment.