mirror of https://github.com/openclaw/openclaw.git
refactor: share teams drive upload flow
This commit is contained in:
parent
e94ac57f80
commit
6b04ab1e35
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js";
|
||||||
|
|
||||||
|
describe("graph upload helpers", () => {
|
||||||
|
const tokenProvider = {
|
||||||
|
getAccessToken: vi.fn(async () => "graph-token"),
|
||||||
|
};
|
||||||
|
|
||||||
|
it("uploads to OneDrive with the personal drive path", async () => {
|
||||||
|
const fetchFn = vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({ id: "item-1", webUrl: "https://example.com/1", name: "a.txt" }),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await uploadToOneDrive({
|
||||||
|
buffer: Buffer.from("hello"),
|
||||||
|
filename: "a.txt",
|
||||||
|
tokenProvider,
|
||||||
|
fetchFn: fetchFn as typeof fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchFn).toHaveBeenCalledWith(
|
||||||
|
"https://graph.microsoft.com/v1.0/me/drive/root:/OpenClawShared/a.txt:/content",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "PUT",
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
Authorization: "Bearer graph-token",
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: "item-1",
|
||||||
|
webUrl: "https://example.com/1",
|
||||||
|
name: "a.txt",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uploads to SharePoint with the site drive path", async () => {
|
||||||
|
const fetchFn = vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({ id: "item-2", webUrl: "https://example.com/2", name: "b.txt" }),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await uploadToSharePoint({
|
||||||
|
buffer: Buffer.from("world"),
|
||||||
|
filename: "b.txt",
|
||||||
|
siteId: "site-123",
|
||||||
|
tokenProvider,
|
||||||
|
fetchFn: fetchFn as typeof fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchFn).toHaveBeenCalledWith(
|
||||||
|
"https://graph.microsoft.com/v1.0/sites/site-123/drive/root:/OpenClawShared/b.txt:/content",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "PUT",
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
Authorization: "Bearer graph-token",
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: "item-2",
|
||||||
|
webUrl: "https://example.com/2",
|
||||||
|
name: "b.txt",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects upload responses missing required fields", async () => {
|
||||||
|
const fetchFn = vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Response(JSON.stringify({ id: "item-3" }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
uploadToSharePoint({
|
||||||
|
buffer: Buffer.from("world"),
|
||||||
|
filename: "bad.txt",
|
||||||
|
siteId: "site-123",
|
||||||
|
tokenProvider,
|
||||||
|
fetchFn: fetchFn as typeof fetch,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("SharePoint upload response missing required fields");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -21,6 +21,53 @@ export interface OneDriveUploadResult {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseUploadedDriveItem(
|
||||||
|
data: { id?: string; webUrl?: string; name?: string },
|
||||||
|
label: "OneDrive" | "SharePoint",
|
||||||
|
): OneDriveUploadResult {
|
||||||
|
if (!data.id || !data.webUrl || !data.name) {
|
||||||
|
throw new Error(`${label} upload response missing required fields`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
webUrl: data.webUrl,
|
||||||
|
name: data.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadDriveItem(params: {
|
||||||
|
buffer: Buffer;
|
||||||
|
filename: string;
|
||||||
|
contentType?: string;
|
||||||
|
tokenProvider: MSTeamsAccessTokenProvider;
|
||||||
|
fetchFn?: typeof fetch;
|
||||||
|
url: string;
|
||||||
|
label: "OneDrive" | "SharePoint";
|
||||||
|
}): Promise<OneDriveUploadResult> {
|
||||||
|
const fetchFn = params.fetchFn ?? fetch;
|
||||||
|
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
|
||||||
|
|
||||||
|
const res = await fetchFn(params.url, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": params.contentType ?? "application/octet-stream",
|
||||||
|
},
|
||||||
|
body: new Uint8Array(params.buffer),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => "");
|
||||||
|
throw new Error(`${params.label} upload failed: ${res.status} ${res.statusText} - ${body}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseUploadedDriveItem(
|
||||||
|
(await res.json()) as { id?: string; webUrl?: string; name?: string },
|
||||||
|
params.label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload a file to the user's OneDrive root folder.
|
* Upload a file to the user's OneDrive root folder.
|
||||||
* For larger files, this uses the simple upload endpoint (up to 4MB).
|
* For larger files, this uses the simple upload endpoint (up to 4MB).
|
||||||
|
|
@ -32,41 +79,13 @@ export async function uploadToOneDrive(params: {
|
||||||
tokenProvider: MSTeamsAccessTokenProvider;
|
tokenProvider: MSTeamsAccessTokenProvider;
|
||||||
fetchFn?: typeof fetch;
|
fetchFn?: typeof fetch;
|
||||||
}): Promise<OneDriveUploadResult> {
|
}): Promise<OneDriveUploadResult> {
|
||||||
const fetchFn = params.fetchFn ?? fetch;
|
|
||||||
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
|
|
||||||
|
|
||||||
// Use "OpenClawShared" folder to organize bot-uploaded files
|
// Use "OpenClawShared" folder to organize bot-uploaded files
|
||||||
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
|
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
|
||||||
|
return await uploadDriveItem({
|
||||||
const res = await fetchFn(`${GRAPH_ROOT}/me/drive/root:${uploadPath}:/content`, {
|
...params,
|
||||||
method: "PUT",
|
url: `${GRAPH_ROOT}/me/drive/root:${uploadPath}:/content`,
|
||||||
headers: {
|
label: "OneDrive",
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": params.contentType ?? "application/octet-stream",
|
|
||||||
},
|
|
||||||
body: new Uint8Array(params.buffer),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.text().catch(() => "");
|
|
||||||
throw new Error(`OneDrive upload failed: ${res.status} ${res.statusText} - ${body}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await res.json()) as {
|
|
||||||
id?: string;
|
|
||||||
webUrl?: string;
|
|
||||||
name?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!data.id || !data.webUrl || !data.name) {
|
|
||||||
throw new Error("OneDrive upload response missing required fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
webUrl: data.webUrl,
|
|
||||||
name: data.name,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OneDriveSharingLink {
|
export interface OneDriveSharingLink {
|
||||||
|
|
@ -175,44 +194,13 @@ export async function uploadToSharePoint(params: {
|
||||||
siteId: string;
|
siteId: string;
|
||||||
fetchFn?: typeof fetch;
|
fetchFn?: typeof fetch;
|
||||||
}): Promise<OneDriveUploadResult> {
|
}): Promise<OneDriveUploadResult> {
|
||||||
const fetchFn = params.fetchFn ?? fetch;
|
|
||||||
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
|
|
||||||
|
|
||||||
// Use "OpenClawShared" folder to organize bot-uploaded files
|
// Use "OpenClawShared" folder to organize bot-uploaded files
|
||||||
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
|
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
|
||||||
|
return await uploadDriveItem({
|
||||||
const res = await fetchFn(
|
...params,
|
||||||
`${GRAPH_ROOT}/sites/${params.siteId}/drive/root:${uploadPath}:/content`,
|
url: `${GRAPH_ROOT}/sites/${params.siteId}/drive/root:${uploadPath}:/content`,
|
||||||
{
|
label: "SharePoint",
|
||||||
method: "PUT",
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": params.contentType ?? "application/octet-stream",
|
|
||||||
},
|
|
||||||
body: new Uint8Array(params.buffer),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.text().catch(() => "");
|
|
||||||
throw new Error(`SharePoint upload failed: ${res.status} ${res.statusText} - ${body}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await res.json()) as {
|
|
||||||
id?: string;
|
|
||||||
webUrl?: string;
|
|
||||||
name?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!data.id || !data.webUrl || !data.name) {
|
|
||||||
throw new Error("SharePoint upload response missing required fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
webUrl: data.webUrl,
|
|
||||||
name: data.name,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMember {
|
export interface ChatMember {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue