List Files through get, because query cannot see a folder before 0.16
Creating a folder on the live 0.15.5 server did nothing visible, with no error and nothing after a reload. The folder was real all along: FileNode/query masks its results with document_ids(false) — resources that are *not* containers — so it returns files and never folders, and says nothing about the omission. FileNode/get carries no such mask, so on those servers the whole tree comes from a single get with ids:null instead. That also stops ensureFolder making a fresh "ihasmail" folder on every signature save, having never been able to find the one already there.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { client } from "@/jmap/client";
|
||||
import { directoryCreate, fileCreate, fileNodeProps, supportsNodeType, normalizeFileNodes } from "../filenode";
|
||||
import { directoryCreate, fileCreate, fileNodeProps, normalizeFileNodes, queryOmitsDirectories, supportsNodeType } from "../filenode";
|
||||
import type { FileNode, JmapSession } from "@/jmap/types";
|
||||
|
||||
/**
|
||||
@@ -117,3 +117,21 @@ describe("rights on a pre-0.16 server", () => {
|
||||
expect(node!.nodeType).toBe("directory");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Before 0.16, FileNode/query masks its results with `document_ids(false)` —
|
||||
* only resources that are *not* containers. It therefore returns files and
|
||||
* never folders, with no error to explain the omission: a folder created there
|
||||
* exists but never comes back in a listing. FileNode/get carries no such mask.
|
||||
*/
|
||||
describe("directory-blind query", () => {
|
||||
it("is worked around on older servers", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
expect(queryOmitsDirectories()).toBe(true);
|
||||
});
|
||||
|
||||
it("is not worked around where query can see folders", () => {
|
||||
client.session = session(NEW_SERVER);
|
||||
expect(queryOmitsDirectories()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,19 @@ export function supportsNodeType(): boolean {
|
||||
|
||||
const BASE_PROPS = ["id", "parentId", "blobId", "size", "name", "type", "created", "modified", "myRights", "role", "executable"];
|
||||
|
||||
/**
|
||||
* Whether `FileNode/query` is blind to directories.
|
||||
*
|
||||
* Before 0.16 the query masks its results with `document_ids(false)`, which
|
||||
* keeps only resources that are *not* containers — so it returns files and
|
||||
* never folders, with no error to say so. A folder created there is real, and
|
||||
* simply never comes back in a listing. `FileNode/get` has no such mask, so
|
||||
* asking it for every id is the only way to see the whole tree.
|
||||
*/
|
||||
export function queryOmitsDirectories(): boolean {
|
||||
return !supportsNodeType();
|
||||
}
|
||||
|
||||
/** Properties to request, asking for `nodeType` only where it exists. */
|
||||
export function fileNodeProps(): string[] {
|
||||
return supportsNodeType() ? [...BASE_PROPS, "nodeType"] : BASE_PROPS;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { CAP, client, setErrorMessage } from "@/jmap/client";
|
||||
import type { FileNode, GetResponse, QueryResponse, SetResponse } from "@/jmap/types";
|
||||
import { directoryCreate, fileCreate, supportsNodeType, normalizeFileNodes } from "@/lib/filenode";
|
||||
import { directoryCreate, fileCreate, normalizeFileNodes, queryOmitsDirectories, supportsNodeType } from "@/lib/filenode";
|
||||
import { useSession } from "@/store/session";
|
||||
import { toast } from "@/ui/toast";
|
||||
|
||||
@@ -17,19 +17,26 @@ const folderProps = () => (supportsNodeType() ? ["id", "name", "nodeType", "pare
|
||||
|
||||
async function ensureFolder(accountId: string): Promise<string> {
|
||||
let list: FileNode[] = [];
|
||||
try {
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, filter: { isTopLevel: true, nodeType: "directory", name: FOLDER }, limit: 5 }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: folderProps() }, "g"],
|
||||
]);
|
||||
list = normalizeFileNodes((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
} catch {
|
||||
// Older servers: no filter support — scan everything.
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, limit: 1000 }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: folderProps() }, "g"],
|
||||
]);
|
||||
list = normalizeFileNodes((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
if (queryOmitsDirectories()) {
|
||||
// Query cannot see a directory on these servers, so it would never find the
|
||||
// folder and we would make a fresh one on every save. Ask get for the lot.
|
||||
const res = await client.call<GetResponse<FileNode>>("FileNode/get", { accountId, ids: null, properties: folderProps() });
|
||||
list = normalizeFileNodes(res.list);
|
||||
} else {
|
||||
try {
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, filter: { isTopLevel: true, nodeType: "directory", name: FOLDER }, limit: 5 }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: folderProps() }, "g"],
|
||||
]);
|
||||
list = normalizeFileNodes((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
} catch {
|
||||
// Filters unsupported: scan everything and pick it out here.
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, limit: 1000 }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: folderProps() }, "g"],
|
||||
]);
|
||||
list = normalizeFileNodes((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
}
|
||||
}
|
||||
const existing = list.find((n) => n.name === FOLDER && n.nodeType === "directory" && !n.parentId);
|
||||
if (existing) return existing.id;
|
||||
|
||||
+20
-13
@@ -1,6 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { CAP, JmapMethodError, client, setErrorMessage } from "@/jmap/client";
|
||||
import { directoryCreate, fileCreate, fileNodeProps, normalizeFileNodes } from "@/lib/filenode";
|
||||
import { directoryCreate, fileCreate, fileNodeProps, normalizeFileNodes, queryOmitsDirectories } from "@/lib/filenode";
|
||||
import type { FileNode, GetResponse, Id, QueryResponse, SetResponse } from "@/jmap/types";
|
||||
import { useSession } from "./session";
|
||||
|
||||
@@ -33,17 +33,24 @@ const byName = (a: FileNode, b: FileNode) => (a.nodeType === b.nodeType ? a.name
|
||||
/** Fetch all nodes (paged, no filter) and rebuild the full children map. */
|
||||
async function loadAllNodes(accountId: Id, set: (fn: (s: FilesState) => Partial<FilesState>) => void): Promise<void> {
|
||||
const all: FileNode[] = [];
|
||||
let position = 0;
|
||||
for (let guard = 0; guard < 100; guard++) {
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, position, limit: 500, calculateTotal: true }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: fileNodeProps() }, "g"],
|
||||
]);
|
||||
const q = res.get("q")?.[0] as unknown as QueryResponse;
|
||||
const g = res.get("g")?.[0] as unknown as GetResponse<FileNode>;
|
||||
all.push(...normalizeFileNodes(g.list));
|
||||
position += q.ids.length;
|
||||
if (!q.ids.length || (q.total != null && position >= q.total)) break;
|
||||
if (queryOmitsDirectories()) {
|
||||
// Query would hand back files only, so every folder — including one just
|
||||
// created — would be missing with nothing to say why. Ask get for the lot.
|
||||
const res = await client.call<GetResponse<FileNode>>("FileNode/get", { accountId, ids: null, properties: fileNodeProps() });
|
||||
all.push(...normalizeFileNodes(res.list));
|
||||
} else {
|
||||
let position = 0;
|
||||
for (let guard = 0; guard < 100; guard++) {
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, position, limit: 500, calculateTotal: true }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: fileNodeProps() }, "g"],
|
||||
]);
|
||||
const q = res.get("q")?.[0] as unknown as QueryResponse;
|
||||
const g = res.get("g")?.[0] as unknown as GetResponse<FileNode>;
|
||||
all.push(...normalizeFileNodes(g.list));
|
||||
position += q.ids.length;
|
||||
if (!q.ids.length || (q.total != null && position >= q.total)) break;
|
||||
}
|
||||
}
|
||||
const nodes: Record<Id, FileNode> = {};
|
||||
const children: Record<string, Id[]> = { root: [] };
|
||||
@@ -77,7 +84,7 @@ export const useFiles = create<FilesState>((set, get) => ({
|
||||
if (!accountId) return;
|
||||
set({ loading: true });
|
||||
try {
|
||||
if (!filtersSupported) {
|
||||
if (!filtersSupported || queryOmitsDirectories()) {
|
||||
await loadAllNodes(accountId, set);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user