Skip to content

Commit 7c743f2

Browse files
authored
Merge branch 'feat/go-fuse-daemon' into refactor/remote-sync-process
2 parents 4e1dfb6 + 6473589 commit 7c743f2

125 files changed

Lines changed: 5629 additions & 506 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 10 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "internxt",
3-
"version": "2.5.4",
3+
"version": "2.6.0",
44
"author": "Internxt <hello@internxt.com>",
55
"description": "Internxt Drive client UI",
66
"main": "./dist/main/main.js",
@@ -15,7 +15,7 @@
1515
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
1616
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir .",
1717
"reinstall:nautilus-extension": "NODE_ENV=development ts-node src/apps/nautilus-extension/reload.ts",
18-
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=60",
18+
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=50",
1919
"lint:fix": "npm run lint --fix",
2020
"format": "prettier src --check",
2121
"format:fix": "prettier src --write",
@@ -81,9 +81,6 @@
8181
"rpm": {
8282
"depends": [
8383
"fuse-libs"
84-
],
85-
"fpm": [
86-
"--rpm-tag", "Recommends: nautilus-python"
8784
]
8885
},
8986
"directories": {
@@ -230,4 +227,4 @@
230227
"engines": {
231228
"node": ">=18.0.0 <19.0.0"
232229
}
233-
}
230+
}

packages/fuse-daemon/internal/client/client.go

Lines changed: 118 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,23 @@ import (
1010
"net"
1111
"net/http"
1212
"time"
13+
14+
"github.com/hanwen/go-fuse/v2/fuse"
1315
)
1416

17+
1518
type Client struct {
16-
http *http.Client
17-
socketPath string
19+
http *http.Client
20+
socketPath string
1821
}
1922

2023
func NewClient(socketPath string) *Client {
21-
return &Client{
22-
http: NewUnixSocketClient(socketPath),
23-
socketPath: socketPath,
24-
}
24+
return &Client{
25+
http: NewUnixSocketClient(socketPath),
26+
socketPath: socketPath,
27+
}
2528
}
2629

27-
2830
func NewUnixSocketClient(socketPath string) *http.Client {
2931
return &http.Client{
3032
Transport: &http.Transport{
@@ -59,35 +61,122 @@ func (client *Client) NotifyReady(logger *slog.Logger) error {
5961
return nil
6062
}
6163

62-
func (client *Client) Post(context context.Context, path OperationPath, in any, out any) error {
63-
body, err := json.Marshal(in)
64-
if err != nil {
65-
return fmt.Errorf("failed to marshal request: %w", err)
66-
}
67-
url := serverURL + string(path)
68-
req, err := http.NewRequestWithContext(context, http.MethodPost, url, bytes.NewBuffer(body))
69-
if err != nil {
70-
return fmt.Errorf("error creating Post request: %w", err)
64+
// Post sends a JSON body to the given operation path and returns an errno.
65+
// A non-200 HTTP response means a transport failure so we return fuse.EIO without reading the body.
66+
// uppon 200, the response always contains an errno field: non-zero means the operation failed with that errno,
67+
// zero means success and the remaining fields are the operation's data, unmarshalled into out if non-nil.
68+
func (client *Client) Post(context context.Context, path OperationPath, in any, out any) fuse.Status {
69+
body, err := json.Marshal(in)
70+
if err != nil {
71+
return fuse.EIO
72+
}
73+
url := serverURL + string(path)
74+
req, err := http.NewRequestWithContext(context, http.MethodPost, url, bytes.NewBuffer(body))
75+
if err != nil {
76+
return fuse.EIO
7177
}
78+
req.Header.Set("Content-Type", "application/json")
7279

73-
req.Header.Set("Content-Type", "application/json")
80+
resp, err := client.http.Do(req)
81+
if err != nil {
82+
return fuse.EIO
83+
}
84+
defer func() { _ = resp.Body.Close() }()
7485

75-
resp, err := client.http.Do(req)
86+
if resp.StatusCode != http.StatusOK {
87+
return fuse.EIO
88+
}
89+
90+
resBody, err := io.ReadAll(resp.Body)
7691
if err != nil {
77-
return fmt.Errorf("sending Post request: %w", err)
92+
return fuse.EIO
93+
}
94+
95+
var errResp ErrorResponse
96+
if err = json.Unmarshal(resBody, &errResp); err == nil && errResp.Errno != 0 {
97+
return fuse.Status(errResp.Errno)
7898
}
79-
defer func() { _ = resp.Body.Close() }()
8099

81-
if resp.StatusCode != http.StatusOK {
82-
return fmt.Errorf("unexpected status from Post endpoint: %d", resp.StatusCode)
100+
if out != nil {
101+
if err = json.Unmarshal(resBody, out); err != nil {
102+
return fuse.EIO
103+
}
104+
}
105+
106+
return fuse.OK
107+
}
108+
109+
// PostBinary sends a JSON body to the given operation path and returns raw binary data.
110+
// Errors are signaled via the X-Errno response header (non-zero = fuse.Status error code).
111+
// On success (X-Errno: 0) the response body is raw bytes copied into dest.
112+
// Returns the number of bytes read and a fuse.Status.
113+
func (client *Client) PostBinary(ctx context.Context, path OperationPath, in any, dest []byte) (int, fuse.Status) {
114+
body, err := json.Marshal(in)
115+
if err != nil {
116+
return 0, fuse.EIO
83117
}
84-
resBody, err := io.ReadAll(resp.Body)
85-
if err != nil {
86-
return fmt.Errorf("failed to read response body: %w", err)
118+
url := serverURL + string(path)
119+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
120+
if err != nil {
121+
return 0, fuse.EIO
87122
}
88-
err = json.Unmarshal(resBody, out)
89-
if err != nil {
90-
return fmt.Errorf("failed to unmarshal response: %w", err)
123+
req.Header.Set("Content-Type", "application/json")
124+
125+
resp, err := client.http.Do(req)
126+
if err != nil {
127+
return 0, fuse.EIO
91128
}
92-
return nil
129+
defer func() { _ = resp.Body.Close() }()
130+
131+
if resp.StatusCode != http.StatusOK {
132+
return 0, fuse.EIO
133+
}
134+
135+
if errnoStr := resp.Header.Get("X-Errno"); errnoStr != "" && errnoStr != "0" {
136+
var errno int32
137+
if _, err := fmt.Sscanf(errnoStr, "%d", &errno); err == nil && errno != 0 {
138+
return 0, fuse.Status(errno)
139+
}
140+
}
141+
142+
bytesRead, err := io.ReadFull(resp.Body, dest)
143+
if err != nil && err != io.ErrUnexpectedEOF {
144+
return 0, fuse.EIO
145+
}
146+
return bytesRead, fuse.OK
147+
}
148+
149+
// PostBinaryRequest sends raw binary payload to the given operation path.
150+
// Errors are signaled via the X-Errno response header (non-zero = fuse.Status error code).
151+
// On success it returns fuse.OK.
152+
func (client *Client) PostSendBinary(ctx context.Context, path OperationPath, payload []byte, headers map[string]string) fuse.Status {
153+
url := serverURL + string(path)
154+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(payload))
155+
if err != nil {
156+
return fuse.EIO
157+
}
158+
req.Header.Set("Content-Type", "application/octet-stream")
159+
for key, value := range headers {
160+
req.Header.Set(key, value)
161+
}
162+
163+
resp, err := client.http.Do(req)
164+
if err != nil {
165+
return fuse.EIO
166+
}
167+
defer func() { _ = resp.Body.Close() }()
168+
169+
if resp.StatusCode != http.StatusOK {
170+
return fuse.EIO
171+
}
172+
173+
if errnoStr := resp.Header.Get("X-Errno"); errnoStr != "" && errnoStr != "0" {
174+
var errno int32
175+
if _, err := fmt.Sscanf(errnoStr, "%d", &errno); err == nil && errno != 0 {
176+
return fuse.Status(errno)
177+
}
178+
return fuse.EIO
179+
}
180+
181+
return fuse.OK
93182
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
package client
2+
23
type OperationPath string
3-
const(
4-
OperationGetAttr OperationPath = "/op/getattributes"
4+
5+
type ErrorResponse struct {
6+
Errno int32 `json:"errno"`
7+
}
8+
9+
const (
10+
OperationGetAttr OperationPath = "/op/getattributes"
11+
OperationOpen OperationPath = "/op/open"
12+
OperationOpenDir OperationPath = "/op/opendir"
13+
OperationRead OperationPath = "/op/read"
14+
OperationTruncate OperationPath = "/op/truncate"
15+
OperationCreate OperationPath = "/op/create"
16+
OperationWrite OperationPath = "/op/write"
17+
OperationRelease OperationPath = "/op/release"
18+
OperationMkdir OperationPath = "/op/mkdir"
19+
OperationRename OperationPath = "/op/rename"
20+
OperationUnlink OperationPath = "/op/unlink"
21+
OperationRmdir OperationPath = "/op/rmdir"
522
)
623

724
const serverURL = "http://localhost"
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package filesystem
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"log/slog"
7+
8+
"internxt/drive-desktop-linux/fuse-daemon/internal/client"
9+
10+
"github.com/hanwen/go-fuse/v2/fuse"
11+
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
12+
)
13+
14+
type WriteCallbackData struct {
15+
Written uint32 `json:"written"`
16+
}
17+
18+
// InternxtFile is the file handle returned by Open.
19+
// It holds the context needed for future Read/Write implementation.
20+
// Operations with a path-based fallback (GetAttr, Chmod, Chown, Truncate, Utimens)
21+
// are intentionally not overridden — DefaultFile returns ENOSYS which triggers
22+
// the fallback to InternxtFilesystem automatically.
23+
type InternxtFile struct {
24+
nodefs.File
25+
path string
26+
flag uint32
27+
processName string
28+
logger *slog.Logger
29+
client *client.Client
30+
}
31+
32+
func NewInternxtFile(path string, flag uint32, processName string, logger *slog.Logger, c *client.Client) *InternxtFile {
33+
return &InternxtFile{
34+
File: nodefs.NewDefaultFile(),
35+
path: path,
36+
flag: flag,
37+
processName: processName,
38+
logger: logger,
39+
client: c,
40+
}
41+
}
42+
43+
func (f *InternxtFile) String() string {
44+
return "InternxtFile(" + f.path + ")"
45+
}
46+
47+
func (f *InternxtFile) Read(dest []byte, off int64) (fuse.ReadResult, fuse.Status) {
48+
f.logger.Debug("Received Read call", "path", f.path, "offset", off, "length", len(dest))
49+
body := struct {
50+
Path string `json:"path"`
51+
Offset int64 `json:"offset"`
52+
Length int `json:"length"`
53+
ProcessName string `json:"processName"`
54+
}{Path: f.path, Offset: off, Length: len(dest), ProcessName: f.processName}
55+
56+
bytesRead, status := f.client.PostBinary(context.Background(), client.OperationRead, body, dest)
57+
if status != fuse.OK {
58+
f.logger.Error("Error occurred while reading file", "status", status)
59+
return nil, status
60+
}
61+
return fuse.ReadResultData(dest[:bytesRead]), fuse.OK
62+
}
63+
64+
func (f *InternxtFile) Write(data []byte, off int64) (uint32, fuse.Status) {
65+
f.logger.Debug("Received Write call", "path", f.path, "offset", off, "length", len(data))
66+
headers := map[string]string{
67+
"X-Path": f.path,
68+
"X-Offset": strconv.FormatInt(off, 10),
69+
}
70+
71+
if status := f.client.PostSendBinary(context.Background(), client.OperationWrite, data, headers); status != fuse.OK {
72+
f.logger.Error("Error occurred while writing file", "status", status)
73+
return 0, status
74+
}
75+
76+
return uint32(len(data)), fuse.OK
77+
}
78+
79+
// Flush is called on each close(2) of the file descriptor.
80+
// Multiple flushes may occur if the file descriptor was duplicated.
81+
// Data is already persisted to the temporal file via Write, so no action is needed here.
82+
func (f *InternxtFile) Flush() fuse.Status {
83+
return fuse.OK
84+
}
85+
86+
func (f *InternxtFile) Release() {
87+
f.logger.Debug("Received Release call:", "path", f.path)
88+
body := struct {
89+
Path string `json:"path"`
90+
ProcessName string `json:"processName"`
91+
}{Path: f.path, ProcessName: f.processName}
92+
if status := f.client.Post(context.Background(), client.OperationRelease, body, nil); status != fuse.OK {
93+
f.logger.Warn("Release call failed", "path", f.path, "status", status)
94+
}
95+
}
96+
97+
func (f *InternxtFile) Fsync(flags int) fuse.Status {
98+
f.logger.Warn("not implemented", "op", "Fsync", "path", f.path)
99+
return fuse.ENOSYS
100+
}

0 commit comments

Comments
 (0)