Skip to content

Drive service saves permanent assets as drive files and renders thumbnails on listing and theme-creator #5698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 14, 2025
105 changes: 84 additions & 21 deletions packages/google-drive-kit/src/board-server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type { GraphTag } from "@breadboard-ai/types";

export { Files };

export type DriveFile = {
Expand All @@ -26,11 +28,10 @@ export type Properties = {
};

export type AppProperties = {
appProperties: {
title: string;
description: string;
tags: string;
};
title: string;
description: string;
tags: GraphTag[];
thumbnailUrl?: string;
};

export type GoogleApiAuthorization =
Expand Down Expand Up @@ -76,22 +77,26 @@ class Files {
return headers;
}

#multipartRequest(metadata: unknown, body: unknown) {
#multipartRequest(
parts: Array<{ contentType: string; data: object | string }>
) {
const boundary = globalThis.crypto.randomUUID();
const headers = this.#makeHeaders();
headers.set("Content-Type", `multipart/related; boundary=${boundary}`);
const multipartBody = `--${boundary}
Content-Type: application/json; charset=UTF-8

${JSON.stringify(metadata, null, 2)}
--${boundary}
Content-Type: application/json; charset=UTF-8

${JSON.stringify(body, null, 2)}
--${boundary}--`;
const body = `--${boundary}\n` + [
...parts.map((part) => {
const data =
typeof part.data === "string"
? part.data
: JSON.stringify(part.data, null, 2);

return `Content-Type: ${part.contentType}\n\n${data}\n`;
}),
'',
].join(`\n--${boundary}`) + `--`;
return {
headers,
body: multipartBody,
body,
};
}

Expand All @@ -104,14 +109,47 @@ ${JSON.stringify(body, null, 2)}

makeQueryRequest(query: string): Request {
return new Request(
this.#makeUrl(`drive/v3/files?q=${encodeURIComponent(query)}&fields=*`),
this.#makeUrl(
`drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name,appProperties,properties)`
),
{
method: "GET",
headers: this.#makeHeaders(),
}
);
}

makeUpdateMetadataRequest(fileId: string, parent: string, metadata: unknown) {
const headers = this.#makeHeaders();
const url = `drive/v3/files/${fileId}?addParents=${parent}`;
return new Request(
this.#makeUrl(url),
{
method: "PATCH",
headers,
body: JSON.stringify(metadata),
}
);
}

makeUploadRequest(fileId: string|undefined, data: string, contentType: string) {
const headers = this.#makeHeaders();
headers.append('Content-Type', contentType);
headers.append('X-Upload-Content-Type', contentType);
headers.append('X-Upload-Content-Length', `${data.length}`);
const url = fileId
? `upload/drive/v3/files/${fileId}?uploadType=media`
: "upload/drive/v3/files?uploadType=media";
return new Request(
this.#makeUrl(url),
{
method: fileId ? "PATCH" : "POST",
headers,
body: b64toBlob(data),
}
);
}

makeLoadRequest(file: string): Request {
return new Request(this.#makeUrl(`drive/v3/files/${file}?alt=media`), {
method: "GET",
Expand All @@ -127,22 +165,27 @@ ${JSON.stringify(body, null, 2)}
});
}

makeMultipartCreateRequest(metadata: unknown, body: unknown): Request {
makeMultipartCreateRequest(
parts: Array<{ contentType: string; data: object | string }>
): Request {
return new Request(
this.#makeUrl(`upload/drive/v3/files?uploadType=multipart`),
{
method: "POST",
...this.#multipartRequest(metadata, body),
...this.#multipartRequest(parts),
}
);
}

makePatchRequest(file: string, metadata: unknown, body: unknown): Request {
makePatchRequest(
file: string,
parts: Array<{ contentType: string; data: object | string }>
): Request {
return new Request(
this.#makeUrl(`upload/drive/v3/files/${file}?uploadType=multipart`),
{
method: "PATCH",
...this.#multipartRequest(metadata, body),
...this.#multipartRequest(parts),
}
);
}
Expand All @@ -154,3 +197,23 @@ ${JSON.stringify(body, null, 2)}
});
}
}

function b64toBlob(b64Data: string, contentType='', sliceSize=512) {
const byteCharacters = atob(b64Data);
const byteArrays = [];

for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}

const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}

const blob = new Blob(byteArrays, {type: contentType});
return blob;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type Outcome,
} from "@google-labs/breadboard";
import type { GoogleDriveClient } from "../google-drive-client.js";
import type { DriveOperations } from "./operations.js";

export { GoogleDriveDataPartTransformer };

Expand All @@ -25,7 +26,10 @@ export type GoogleDriveToGeminiResponse = {
};

class GoogleDriveDataPartTransformer implements DataPartTransformer {
constructor(public readonly client: GoogleDriveClient) {}
constructor(
public readonly client: GoogleDriveClient,
private readonly ops: DriveOperations
) {}

async #createRequest(path: string, body: unknown): Promise<Request> {
return new Request(path, {
Expand All @@ -40,7 +44,7 @@ class GoogleDriveDataPartTransformer implements DataPartTransformer {

async persistPart(
_graphUrl: URL,
_part: InlineDataCapabilityPart,
part: InlineDataCapabilityPart,
temporary: boolean
): Promise<Outcome<StoredDataCapabilityPart>> {
if (temporary) {
Expand All @@ -52,11 +56,16 @@ class GoogleDriveDataPartTransformer implements DataPartTransformer {
console.debug(msg);
return err(msg);
} else {
// This is is most likely the situation when a new BGL asset is saved
// and being persisted.
const msg = `Persisting assets is not supported with Google Drive backend`;
console.debug(msg);
return err(msg);
const driveFileUrl = await this.ops.saveDataPart(
part.inlineData.data,
part.inlineData.mimeType,
);
return {
storedData: {
handle: driveFileUrl,
mimeType: part.inlineData.mimeType,
},
};
}
}

Expand Down
Loading