From 621fbe4913692edea2ee453a9418416f28be9438 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 13 Jul 2026 23:07:30 +0200 Subject: [PATCH] Fix FUSE attachment sizes and item filenames --- docspell_client.go | 30 +++++++++++++++++++++++++++++- mount.go | 31 +++++++++++++++++++++++-------- mount_test.go | 12 ++++++------ 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/docspell_client.go b/docspell_client.go index ab61c24..f24d392 100644 --- a/docspell_client.go +++ b/docspell_client.go @@ -137,6 +137,30 @@ func (c *DocspellClient) GetItem(ctx context.Context, id string) (*SearchItem, e return &out, nil } +func (c *DocspellClient) AttachmentSize(ctx context.Context, id string) (int64, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, c.url("/api/v1/sec/attachment/"+url.PathEscape(id)), nil) + if err != nil { + return 0, err + } + if c.Token != "" { + req.Header.Set(c.AuthHeader, c.Token) + } + httpClient := c.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + resp, err := httpClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return 0, fmt.Errorf("%s %s: %s: %s", req.Method, req.URL, resp.Status, strings.TrimSpace(string(body))) + } + return resp.ContentLength, nil +} + func (c *DocspellClient) DownloadAttachment(ctx context.Context, id string) ([]byte, error) { return c.doBytes(ctx, http.MethodGet, "/api/v1/sec/attachment/"+url.PathEscape(id), nil) } @@ -179,7 +203,11 @@ func (c *DocspellClient) do(req *http.Request) ([]byte, error) { if c.Token != "" { req.Header.Set(c.AuthHeader, c.Token) } - resp, err := c.HTTPClient.Do(req) + httpClient := c.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + resp, err := httpClient.Do(req) if err != nil { return nil, err } diff --git a/mount.go b/mount.go index 13d5de2..f43a9d3 100644 --- a/mount.go +++ b/mount.go @@ -54,7 +54,7 @@ func runMountCommand(args []string, cfg config, password string) error { if err != nil { return fmt.Errorf("search items: %w", err) } - root := buildMountTree(client, items) + root := buildMountTree(context.Background(), client, items) conn, err := fuse.Mount( fsFlags.Arg(0), @@ -71,9 +71,12 @@ func runMountCommand(args []string, cfg config, password string) error { return fs.Serve(conn, &docspellFS{root: root}) } -func buildMountTree(client *DocspellClient, items []SearchItem) *dirNode { +func buildMountTree(ctx context.Context, client *DocspellClient, items []SearchItem) *dirNode { root := &dirNode{name: "", children: map[string]fs.Node{}} for _, item := range items { + if full, err := client.GetItem(ctx, item.ID); err == nil { + item = *full + } folderName := "No Folder" if item.Folder != nil && item.Folder.Name != "" { folderName = item.Folder.Name @@ -94,15 +97,22 @@ func buildMountTree(client *DocspellClient, items []SearchItem) *dirNode { base = item.ID } for i, att := range item.Attachments { - name := sanitizeName(att.Name) - if name == "" { - name = fmt.Sprintf("%s-%d.pdf", base, i+1) + ext := filepath.Ext(att.Name) + if ext == "" { + ext = ".pdf" } + name := base + ext if len(item.Attachments) > 1 { - name = fmt.Sprintf("%s - %02d - %s", base, i+1, name) + name = fmt.Sprintf("%s - %02d%s", base, i+1, ext) } - name = uniqueName(dir.children, name) - dir.children[name] = &fileNode{name: name, attachmentID: att.ID, size: uint64(att.Size), knownSize: att.Size > 0, modTime: modTime, client: client} + size := att.Size + if size <= 0 { + if headSize, err := client.AttachmentSize(ctx, att.ID); err == nil && headSize > 0 { + size = headSize + } + } + name = uniqueName(dir.children, sanitizeName(name)) + dir.children[name] = &fileNode{name: name, attachmentID: att.ID, size: uint64(size), knownSize: size > 0, modTime: modTime, client: client} } } return root @@ -189,6 +199,11 @@ func (f *fileNode) Attr(ctx context.Context, a *fuse.Attr) error { return nil } +func (f *fileNode) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) error { + resp.Flags |= fuse.OpenDirectIO + return nil +} + func (f *fileNode) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { data, err := f.load(ctx) if err != nil { diff --git a/mount_test.go b/mount_test.go index cb017bc..edf925c 100644 --- a/mount_test.go +++ b/mount_test.go @@ -18,7 +18,7 @@ func TestBuildMountTreeGroupsByFolderAndDate(t *testing.T) { Attachments: []ItemAttachment{{ID: "att-1", Name: "invoice.pdf", Size: 123}}, }} - root := buildMountTree(&DocspellClient{}, items) + root := buildMountTree(context.Background(), &DocspellClient{}, items) folder, ok := root.children["Bills_Private"].(*dirNode) if !ok { t.Fatalf("expected sanitized folder directory, got %#v", root.children) @@ -31,8 +31,8 @@ func TestBuildMountTreeGroupsByFolderAndDate(t *testing.T) { if !ok { t.Fatalf("expected month directory, got %#v", year.children) } - if _, ok := month.children["invoice.pdf"].(*fileNode); !ok { - t.Fatalf("expected attachment file, got %#v", month.children) + if _, ok := month.children["Invoice_July.pdf"].(*fileNode); !ok { + t.Fatalf("expected item-named attachment file, got %#v", month.children) } } @@ -46,13 +46,13 @@ func TestBuildMountTreeDeduplicatesAttachmentNames(t *testing.T) { }, }} - root := buildMountTree(&DocspellClient{}, items) + root := buildMountTree(context.Background(), &DocspellClient{}, items) folder := root.children["No Folder"].(*dirNode) date := folder.children["No Date"].(*dirNode) - if _, ok := date.children["Invoice - 01 - same.pdf"]; !ok { + if _, ok := date.children["Invoice - 01.pdf"]; !ok { t.Fatalf("expected first unique name, got %#v", date.children) } - if _, ok := date.children["Invoice - 02 - same.pdf"]; !ok { + if _, ok := date.children["Invoice - 02.pdf"]; !ok { t.Fatalf("expected second unique name, got %#v", date.children) } }