mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
4090483738
* chore(deps): bump fast-xml-parser and @aws-sdk/xml-builder Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) and [@aws-sdk/xml-builder](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/xml-builder). These dependencies needed to be updated together. Updates `fast-xml-parser` from 5.3.4 to 5.5.8 - [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases) - [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.4...v5.5.8) Updates `@aws-sdk/xml-builder` from 3.972.4 to 3.972.18 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/xml-builder/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/xml-builder) --- updated-dependencies: - dependency-name: fast-xml-parser dependency-version: 5.5.8 dependency-type: indirect - dependency-name: "@aws-sdk/xml-builder" dependency-version: 3.972.18 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump follow-redirects from 1.15.11 to 1.16.0 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0) --- updated-dependencies: - dependency-name: follow-redirects dependency-version: 1.16.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump hono from 4.12.7 to 4.12.14 Bumps [hono](https://github.com/honojs/hono) from 4.12.7 to 4.12.14. - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.7...v4.12.14) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.14 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump vite from 7.3.1 to 7.3.2 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump flatted from 3.3.3 to 3.4.2 Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2. - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) --- updated-dependencies: - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump minimatch from 3.1.2 to 3.1.5 Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump lodash from 4.17.23 to 4.18.1 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump @hono/node-server from 1.19.11 to 1.19.14 Bumps [@hono/node-server](https://github.com/honojs/node-server) from 1.19.11 to 1.19.14. - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](https://github.com/honojs/node-server/compare/v1.19.11...v1.19.14) --- updated-dependencies: - dependency-name: "@hono/node-server" dependency-version: 1.19.14 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump rollup from 4.57.1 to 4.60.2 Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.60.2. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.60.2) --- updated-dependencies: - dependency-name: rollup dependency-version: 4.60.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump electron from 40.1.0 to 40.8.5 Bumps [electron](https://github.com/electron/electron) from 40.1.0 to 40.8.5. - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v40.1.0...v40.8.5) --- updated-dependencies: - dependency-name: electron dependency-version: 40.8.5 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump path-to-regexp from 8.3.0 to 8.4.2 Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 8.3.0 to 8.4.2. - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v8.3.0...v8.4.2) --- updated-dependencies: - dependency-name: path-to-regexp dependency-version: 8.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump picomatch from 2.3.1 to 2.3.2 Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) --- updated-dependencies: - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump yaml from 2.8.2 to 2.8.3 Bumps [yaml](https://github.com/eemeli/yaml) from 2.8.2 to 2.8.3. - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.8.2...v2.8.3) --- updated-dependencies: - dependency-name: yaml dependency-version: 2.8.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump @xmldom/xmldom from 0.8.11 to 0.8.13 Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.8.11 to 0.8.13. - [Release notes](https://github.com/xmldom/xmldom/releases) - [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md) - [Commits](https://github.com/xmldom/xmldom/compare/0.8.11...0.8.13) --- updated-dependencies: - dependency-name: "@xmldom/xmldom" dependency-version: 0.8.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump brace-expansion from 1.1.12 to 1.1.14 Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.14. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.14) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.14 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): bump tar from 7.5.7 to 7.5.13 Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.13. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.13) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Security Fixes Security fixes: Added input validation for uncontrolled command lines. Added Proper Shell Escaping for useTerminalAutocomplete Fixed 4 race condition alerts by atomic stat+read(s) without following symlinks. Misc: Use Crypto randomness instead of Math.random() (Not a security issue but convenient) * Fix OS quirk fallbacks * Review fix - use lstat before open to skip FIFO/devices early to prevent blocks - SFTP skip UUID tag could be dubiously long * allow symlinks alongside regular files. * Use acutal target size for reading * Fix Destructed import / fix to use full shellEscape charset - Destructed import - Guard now matches full shellEscape charset * Supress Codex complaints Replaced manual fd.read with fs.promises.readFile(fd) to ensure complete file reads to EOF. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
418 lines
14 KiB
JavaScript
418 lines
14 KiB
JavaScript
/**
|
|
* Google OAuth Bridge (main process)
|
|
*
|
|
* Renderer fetches to Google's OAuth token endpoint can be blocked by CORS.
|
|
* This bridge proxies Google token exchange/refresh and userinfo via the main process.
|
|
*/
|
|
|
|
const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
const GOOGLE_DRIVE_API = "https://www.googleapis.com/drive/v3";
|
|
const GOOGLE_DRIVE_UPLOAD_API = "https://www.googleapis.com/upload/drive/v3";
|
|
const DEFAULT_SYNC_FILE_NAME = "netcatty-vault.json";
|
|
const { randomUUID } = require("node:crypto");
|
|
|
|
const isNonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
|
|
|
|
const describeNetworkError = (err) => {
|
|
const msg = err?.message || String(err);
|
|
const cause = err?.cause;
|
|
const causeCode = cause?.code ? ` (${cause.code})` : "";
|
|
const causeMsg = cause?.message ? `: ${cause.message}` : "";
|
|
return `${msg}${causeCode}${causeMsg}`.trim();
|
|
};
|
|
|
|
const safeJsonParse = (text) => {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const describeGoogleApiError = (status, text) => {
|
|
const data = safeJsonParse(text);
|
|
const msg =
|
|
data?.error?.message ||
|
|
data?.error_description ||
|
|
data?.error ||
|
|
(typeof text === "string" && text.trim().length ? text.trim() : "Unknown error");
|
|
return `${status} - ${String(msg).slice(0, 300)}`;
|
|
};
|
|
|
|
/**
|
|
* @param {Electron.IpcMain} ipcMain
|
|
* @param {import('electron')=} electronModule
|
|
*/
|
|
function registerHandlers(ipcMain, electronModule) {
|
|
const fetchImpl =
|
|
electronModule?.net?.fetch ? electronModule.net.fetch.bind(electronModule.net) : fetch;
|
|
|
|
ipcMain.handle("netcatty:google:oauth:exchange", async (_event, payload) => {
|
|
const clientId = payload?.clientId;
|
|
const clientSecret = payload?.clientSecret;
|
|
const code = payload?.code;
|
|
const codeVerifier = payload?.codeVerifier;
|
|
const redirectUri = payload?.redirectUri;
|
|
|
|
if (!isNonEmptyString(clientId)) throw new Error("Missing Google clientId");
|
|
if (!isNonEmptyString(code)) throw new Error("Missing authorization code");
|
|
if (!isNonEmptyString(codeVerifier)) throw new Error("Missing codeVerifier");
|
|
if (!isNonEmptyString(redirectUri)) throw new Error("Missing redirectUri");
|
|
|
|
const body = new URLSearchParams({
|
|
client_id: clientId,
|
|
code,
|
|
code_verifier: codeVerifier,
|
|
grant_type: "authorization_code",
|
|
redirect_uri: redirectUri,
|
|
});
|
|
if (isNonEmptyString(clientSecret)) {
|
|
body.set("client_secret", clientSecret);
|
|
}
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(GOOGLE_TOKEN_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: body.toString(),
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google token exchange network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
data = { error: text };
|
|
}
|
|
throw new Error(`Google token exchange failed: ${data.error_description || data.error || res.status}`);
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
throw new Error(`Google token exchange invalid JSON: ${text.slice(0, 200)}`);
|
|
}
|
|
|
|
return {
|
|
accessToken: data.access_token,
|
|
refreshToken: data.refresh_token,
|
|
expiresAt: Date.now() + (data.expires_in || 0) * 1000,
|
|
tokenType: data.token_type,
|
|
scope: data.scope,
|
|
};
|
|
});
|
|
|
|
ipcMain.handle("netcatty:google:oauth:refresh", async (_event, payload) => {
|
|
const clientId = payload?.clientId;
|
|
const clientSecret = payload?.clientSecret;
|
|
const refreshToken = payload?.refreshToken;
|
|
|
|
if (!isNonEmptyString(clientId)) throw new Error("Missing Google clientId");
|
|
if (!isNonEmptyString(refreshToken)) throw new Error("Missing refreshToken");
|
|
|
|
const body = new URLSearchParams({
|
|
client_id: clientId,
|
|
refresh_token: refreshToken,
|
|
grant_type: "refresh_token",
|
|
});
|
|
if (isNonEmptyString(clientSecret)) {
|
|
body.set("client_secret", clientSecret);
|
|
}
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(GOOGLE_TOKEN_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: body.toString(),
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google token refresh network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
data = { error: text };
|
|
}
|
|
throw new Error(`Google token refresh failed: ${data.error_description || data.error || res.status}`);
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
throw new Error(`Google token refresh invalid JSON: ${text.slice(0, 200)}`);
|
|
}
|
|
|
|
return {
|
|
accessToken: data.access_token,
|
|
refreshToken,
|
|
expiresAt: Date.now() + (data.expires_in || 0) * 1000,
|
|
tokenType: data.token_type,
|
|
scope: data.scope,
|
|
};
|
|
});
|
|
|
|
ipcMain.handle("netcatty:google:oauth:userinfo", async (_event, payload) => {
|
|
const accessToken = payload?.accessToken;
|
|
if (!isNonEmptyString(accessToken)) throw new Error("Missing accessToken");
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(GOOGLE_USERINFO_URL, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google userinfo network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
throw new Error(`Google userinfo failed: ${res.status} - ${text.slice(0, 200)}`);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
throw new Error(`Google userinfo invalid JSON: ${text.slice(0, 200)}`);
|
|
}
|
|
});
|
|
|
|
// Google Drive API (appDataFolder) - proxied to avoid CORS/COEP issues in renderer
|
|
ipcMain.handle("netcatty:google:drive:findSyncFile", async (_event, payload) => {
|
|
const accessToken = payload?.accessToken;
|
|
const fileName = isNonEmptyString(payload?.fileName) ? payload.fileName : DEFAULT_SYNC_FILE_NAME;
|
|
if (!isNonEmptyString(accessToken)) throw new Error("Missing accessToken");
|
|
|
|
const params = new URLSearchParams({
|
|
spaces: "appDataFolder",
|
|
q: `name = '${fileName}'`,
|
|
fields: "files(id, name, modifiedTime)",
|
|
});
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(`${GOOGLE_DRIVE_API}/files?${params.toString()}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google Drive network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
if (res.status === 403) {
|
|
throw new Error("Google Drive API not enabled. Please enable it in Google Cloud Console.");
|
|
}
|
|
if (res.status === 401) {
|
|
throw new Error("Token expired or invalid. Please reconnect Google Drive.");
|
|
}
|
|
throw new Error(`Google Drive API error: ${describeGoogleApiError(res.status, text)}`);
|
|
}
|
|
|
|
const data = safeJsonParse(text);
|
|
const fileId = data?.files?.[0]?.id || null;
|
|
return { fileId };
|
|
});
|
|
|
|
ipcMain.handle("netcatty:google:drive:createSyncFile", async (_event, payload) => {
|
|
const accessToken = payload?.accessToken;
|
|
const fileName = isNonEmptyString(payload?.fileName) ? payload.fileName : DEFAULT_SYNC_FILE_NAME;
|
|
const syncedFile = payload?.syncedFile;
|
|
if (!isNonEmptyString(accessToken)) throw new Error("Missing accessToken");
|
|
if (!syncedFile) throw new Error("Missing syncedFile");
|
|
|
|
const boundary = `----netcatty_${randomUUID()}`;
|
|
const metadata = JSON.stringify({
|
|
name: fileName,
|
|
parents: ["appDataFolder"],
|
|
});
|
|
const content = JSON.stringify(syncedFile, null, 2);
|
|
|
|
const body =
|
|
`--${boundary}\r\n` +
|
|
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
|
|
`${metadata}\r\n` +
|
|
`--${boundary}\r\n` +
|
|
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
|
|
`${content}\r\n` +
|
|
`--${boundary}--\r\n`;
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(`${GOOGLE_DRIVE_UPLOAD_API}/files?uploadType=multipart`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
},
|
|
body,
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google Drive upload network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
if (res.status === 403) {
|
|
throw new Error("Google Drive API not enabled. Please enable it in Google Cloud Console.");
|
|
}
|
|
if (res.status === 401) {
|
|
throw new Error("Token expired or invalid. Please reconnect Google Drive.");
|
|
}
|
|
throw new Error(`Google Drive upload error: ${describeGoogleApiError(res.status, text)}`);
|
|
}
|
|
|
|
const data = safeJsonParse(text);
|
|
const fileId = data?.id;
|
|
if (!isNonEmptyString(fileId)) {
|
|
throw new Error(`Google Drive upload invalid response: ${text.slice(0, 200)}`);
|
|
}
|
|
|
|
return { fileId };
|
|
});
|
|
|
|
ipcMain.handle("netcatty:google:drive:updateSyncFile", async (_event, payload) => {
|
|
const accessToken = payload?.accessToken;
|
|
const fileId = payload?.fileId;
|
|
const syncedFile = payload?.syncedFile;
|
|
if (!isNonEmptyString(accessToken)) throw new Error("Missing accessToken");
|
|
if (!isNonEmptyString(fileId)) throw new Error("Missing fileId");
|
|
if (!syncedFile) throw new Error("Missing syncedFile");
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(`${GOOGLE_DRIVE_UPLOAD_API}/files/${encodeURIComponent(fileId)}?uploadType=media`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(syncedFile, null, 2),
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google Drive update network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
if (res.status === 403) {
|
|
throw new Error("Google Drive API not enabled. Please enable it in Google Cloud Console.");
|
|
}
|
|
if (res.status === 401) {
|
|
throw new Error("Token expired or invalid. Please reconnect Google Drive.");
|
|
}
|
|
throw new Error(`Google Drive update error: ${describeGoogleApiError(res.status, text)}`);
|
|
}
|
|
|
|
return { ok: true };
|
|
});
|
|
|
|
ipcMain.handle("netcatty:google:drive:downloadSyncFile", async (_event, payload) => {
|
|
const accessToken = payload?.accessToken;
|
|
const fileId = payload?.fileId;
|
|
if (!isNonEmptyString(accessToken)) throw new Error("Missing accessToken");
|
|
if (!isNonEmptyString(fileId)) throw new Error("Missing fileId");
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(`${GOOGLE_DRIVE_API}/files/${encodeURIComponent(fileId)}?alt=media`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google Drive download network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
if (res.status === 404) return { syncedFile: null };
|
|
if (res.status === 401) {
|
|
throw new Error("Token expired or invalid. Please reconnect Google Drive.");
|
|
}
|
|
throw new Error(`Google Drive download error: ${describeGoogleApiError(res.status, text)}`);
|
|
}
|
|
|
|
const data = safeJsonParse(text);
|
|
if (!data) {
|
|
throw new Error(`Google Drive download invalid JSON: ${text.slice(0, 200)}`);
|
|
}
|
|
|
|
return { syncedFile: data };
|
|
});
|
|
|
|
ipcMain.handle("netcatty:google:drive:deleteSyncFile", async (_event, payload) => {
|
|
const accessToken = payload?.accessToken;
|
|
const fileId = payload?.fileId;
|
|
if (!isNonEmptyString(accessToken)) throw new Error("Missing accessToken");
|
|
if (!isNonEmptyString(fileId)) throw new Error("Missing fileId");
|
|
|
|
let res;
|
|
try {
|
|
res = await fetchImpl(`${GOOGLE_DRIVE_API}/files/${encodeURIComponent(fileId)}`, {
|
|
method: "DELETE",
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Google Drive delete network error. Check your network/VPN and whether Google services are reachable. (${describeNetworkError(err)})`
|
|
);
|
|
}
|
|
|
|
if (!res.ok && res.status !== 404) {
|
|
const text = await res.text();
|
|
if (res.status === 401) {
|
|
throw new Error("Token expired or invalid. Please reconnect Google Drive.");
|
|
}
|
|
throw new Error(`Google Drive delete error: ${describeGoogleApiError(res.status, text)}`);
|
|
}
|
|
|
|
return { ok: true };
|
|
});
|
|
}
|
|
|
|
module.exports = { registerHandlers };
|