Skip to content

Commit 5f0c8b1

Browse files
committed
Fix binary files failing to upload to GitHub
1 parent c1d1db6 commit 5f0c8b1

File tree

4 files changed

+145
-3
lines changed

4 files changed

+145
-3
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@codemirror/view": "^6.36.2",
3030
"@uiw/react-codemirror": "^4.23.8",
3131
"codemirror": "^6.0.1",
32+
"file-type": "^20.4.1",
3233
"react": "^19.0.0",
3334
"react-dom": "^19.0.0"
3435
}

pnpm-lock.yaml

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

src/github/client.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export type NewTreeRequestItem = {
2727
content?: string;
2828
};
2929

30+
/**
31+
* Response received when we create a new binary blob on GitHub
32+
*/
33+
export type CreatedBlob = {
34+
sha: string;
35+
};
36+
3037
/**
3138
* Represents a git blob response from the GitHub API.
3239
*/
@@ -170,6 +177,36 @@ export default class GithubClient {
170177
}
171178
}
172179

180+
/**
181+
* Creates a new blob in the GitHub remote, this is mainly used to upload binary files.
182+
*
183+
* @param content The content of the blob to upload
184+
* @param encoding Content encoding, can be "utf-8" or "base64". Defaults to "base64"
185+
* @returns The SHA of the newly uploaded blob
186+
*/
187+
async createBlob(
188+
content: string,
189+
encoding: "utf-8" | "base64" = "base64",
190+
): Promise<CreatedBlob> {
191+
const res = await requestUrl({
192+
url: `https://api.github.com/repos/${this.settings.githubOwner}/${this.settings.githubRepo}/git/blobs`,
193+
headers: this.headers(),
194+
method: "POST",
195+
body: JSON.stringify({ content, encoding }),
196+
throw: false,
197+
});
198+
if (res.status < 200 || res.status >= 400) {
199+
await this.logger.error("Failed to create blob", res);
200+
throw new GithubAPIError(
201+
res.status,
202+
`Failed to create blob, status ${res.status}`,
203+
);
204+
}
205+
return {
206+
sha: res.json["sha"],
207+
};
208+
}
209+
173210
/**
174211
* Gets a blob from its sha
175212
* @param url blob sha

src/sync-manager.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
normalizePath,
55
base64ToArrayBuffer,
66
EventRef,
7+
arrayBufferToBase64,
78
} from "obsidian";
89
import GithubClient, {
910
GetTreeResponseItem,
@@ -20,6 +21,7 @@ import { GitHubSyncSettings } from "./settings/settings";
2021
import Logger from "./logger";
2122
import { decodeBase64String } from "./utils";
2223
import GitHubSyncPlugin from "./main";
24+
import { fileTypeFromBuffer } from "file-type";
2325

2426
interface SyncAction {
2527
type: "upload" | "download" | "delete_local" | "delete_remote";
@@ -666,8 +668,8 @@ export default class SyncManager {
666668
* @returns String containing the file SHA1
667669
*/
668670
async calculateSHA(filePath: string): Promise<string> {
669-
const content = await this.vault.adapter.read(filePath);
670-
const contentBytes = new TextEncoder().encode(content);
671+
const contentBuffer = await this.vault.adapter.readBinary(filePath);
672+
const contentBytes = new Uint8Array(contentBuffer);
671673
const header = new TextEncoder().encode(`blob ${contentBytes.length}\0`);
672674
const store = new Uint8Array([...header, ...contentBytes]);
673675
return await crypto.subtle.digest("SHA-1", store).then((hash) =>
@@ -713,11 +715,40 @@ export default class SyncManager {
713715
// We don't save the metadata file after setting the SHAs cause we do that when
714716
// the sync is fully commited at the end.
715717
// TODO: Understand whether it's a problem we don't revert the SHA setting in case of sync failure
718+
//
719+
// In here we also upload blob is file is a binary. We do it here because when uploading a blob we
720+
// also get back its SHA, so we can set it together with other files.
721+
// We also do that right before creating the new tree because we need the SHAs of those blob to
722+
// correctly create it.
716723
await Promise.all(
717724
Object.keys(treeFiles)
718725
.filter((filePath: string) => treeFiles[filePath].content)
719726
.map(async (filePath: string) => {
720-
const newSha = await this.calculateSHA(filePath);
727+
const buffer = await this.vault.adapter.readBinary(filePath);
728+
const fileType = await fileTypeFromBuffer(buffer);
729+
let newSha = "";
730+
if (
731+
// We can't determine the file type
732+
fileType === undefined ||
733+
// This is not a text file
734+
!fileType.mime.startsWith("text/") ||
735+
// Neither a json file
736+
fileType.mime !== "application/json"
737+
) {
738+
// We treat this file as a binary file. We can't upload these setting the content
739+
// of a tree item, we first need to create a Git blob by uploading the file, then
740+
// we must update the tree item to point the SHA to the blob we just created.
741+
const hash = arrayBufferToBase64(buffer);
742+
const { sha } = await this.client.createBlob(hash);
743+
treeFiles[filePath].sha = sha;
744+
// Can't have both sha and content set, so we delete it
745+
delete treeFiles[filePath].content;
746+
newSha = sha;
747+
} else {
748+
// File is text, we can upload the content directly
749+
// so we just calculate the new SHA to keep track of it
750+
newSha = await this.calculateSHA(filePath);
751+
}
721752
this.metadataStore.data.files[filePath].sha = newSha;
722753
}),
723754
);

0 commit comments

Comments
 (0)