@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
295 lines (294 loc) • 9.9 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unused-vars */
function sanitizeLogInput(input) {
// Remove newline characters and escape quotes/backslashes
return input.replace(/[\n\r]/g, '').replace(/["'\\]/g, '\\$&');
}
import process from 'node:process';
import { trace, context } from '@opentelemetry/api';
import { instrumentFunction } from '../telemetry.js';
export class GoogleDriveServiceError extends Error {
constructor(msg, params) {
super(msg);
Object.defineProperty(this, "status", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "file", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "folderId", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "isQuotaError", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
if (params) {
this.status = params.status;
this.isQuotaError = params.isQuotaError;
this.file = params.file;
this.folderId = params.folderId;
}
}
}
function jsonToErrorMessage(json) {
try {
json = JSON.parse(json);
}
catch (err) {
return json;
}
if (typeof json === 'string') {
return json;
}
if (json.error) {
const driveError = json;
if (driveError.error.errors && Array.isArray(driveError.error.errors)) {
return driveError.error.errors
.map(error => {
if (error.message) {
return error.message;
}
return JSON.stringify(error);
})
.join('\n');
}
if (driveError.error.message && typeof driveError.error.message === 'string') {
return driveError.error.message;
}
if ('string' === typeof driveError.error) {
return driveError.error;
}
}
}
export function filterParams(params) {
const retVal = {};
for (const key of Object.keys(params)) {
const value = params[key];
if ('undefined' === typeof value) {
continue;
}
retVal[key] = value;
}
return retVal;
}
export async function convertResponseToError(response, requestInfo) {
const body = await response.text();
const message = jsonToErrorMessage(body) || response.statusText;
let isQuotaError = false;
if (429 === response.status) {
isQuotaError = true;
}
if (message.indexOf('User Rate Limit Exceeded') > -1) {
isQuotaError = true;
}
let isUnautorized = false;
if (401 === response.status) {
isUnautorized = true;
}
if (process.env.VERSION === 'dev' && !isUnautorized) {
console.trace();
if (requestInfo) {
const sanitizedRequestInfo = typeof requestInfo === 'string' ? sanitizeLogInput(requestInfo) : requestInfo;
console.error('convertResponseToError requestInfo', sanitizedRequestInfo);
}
console.error('convertResponseToError response', response.status, response.statusText, response.headers);
console.error('convertResponseToError body', body);
}
return new GoogleDriveServiceError(message, {
status: response.status,
isQuotaError
// file,
// folderId
});
}
async function driveRequest(quotaLimiter, accessToken, method, requestUrl, params, body) {
const filteredParams = filterParams(params);
const headers = {
"User-Agent": "wikigdrive (gzip)",
Authorization: "Bearer " + accessToken,
"Accept-Encoding": "gzip",
};
if (body) {
headers["Content-type"] = "application/json";
}
let traceparent = "";
if (process.env.ZIPKIN_URL) {
const span = trace.getActiveSpan();
if (span) {
traceparent = span.spanContext().traceId;
}
}
if (traceparent) {
headers["traceparent"] = traceparent;
}
const url = requestUrl + "?" + new URLSearchParams(filteredParams).toString();
if (!quotaLimiter) {
const fetchInstrumented = instrumentFunction(fetch, 1);
const response = await fetchInstrumented(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (response.status >= 400) {
throw await convertResponseToError(response, url);
}
return response;
}
return await new Promise(async (resolve, reject) => {
const job = async () => {
try {
const fetchInstrumented = instrumentFunction(fetch, 1);
const response = await fetchInstrumented(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (response.status >= 400) {
return reject(await convertResponseToError(response, url));
}
resolve(response);
}
catch (err) {
reject(err);
}
};
if (requestUrl.endsWith('drive/v3/files')) {
job.skipCounter = true;
}
if (process.env.ZIPKIN_URL) {
job.parentCtx = context.active();
}
quotaLimiter.addJob(job);
});
}
export async function driveFetch(quotaLimiter, accessToken, method, url, params, bodyReq) {
const response = await driveRequest(quotaLimiter, accessToken, method, url, params, bodyReq);
let bodyResp = '';
try {
bodyResp = await response.text();
return JSON.parse(bodyResp);
}
catch (err) {
throw new Error('Invalid JSON: ' + url + ', ' + bodyResp);
}
}
export async function driveFetchStream(quotaLimiter, accessToken, method, url, params) {
const response = await driveRequest(quotaLimiter, accessToken, method, url, params);
return response.body;
}
const boundary = '-------314159265358979323846';
export async function driveFetchMultipart(quotaLimiter, accessToken, method, requestUrl, params, formData) {
const filteredParams = filterParams(params);
const url = requestUrl + '?' + new URLSearchParams(filteredParams).toString();
let traceparent;
if (process.env.ZIPKIN_URL) {
const span = trace.getActiveSpan();
if (span) {
traceparent = span.spanContext().traceId;
}
}
const after = `\n--${boundary}--`;
function generateMultipart(image, mimetype) {
const source = new Uint8Array(image); // Wrap in view to get data
const before = [
`\n--${boundary}\n`,
`Content-Type: ${mimetype}\n`,
`Content-Length: ${source.byteLength}\n`,
'\n'
].join('');
const size = before.length + source.byteLength;
const uint8array = new Uint8Array(size);
for (let i = 0; i < before.length; i++) {
uint8array[i] = before.charCodeAt(i) & 0xff;
}
for (let j = 0; j < source.byteLength; j++) {
uint8array[j + before.length] = source[j];
}
return uint8array.buffer;
}
const arr = [];
formData.forEach((entry) => {
if (typeof entry !== 'string') {
const blob = entry;
arr.push(blob);
}
});
const body = [];
for (const blob of arr) {
const buff = generateMultipart(await blob.arrayBuffer(), blob.type);
body.push(buff);
}
body.push(new TextEncoder().encode(after));
if (!quotaLimiter) {
const buff = await new Blob(body).arrayBuffer();
const response = await fetch(url, {
method,
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-Type': `multipart/related; boundary="${boundary}"`,
'Content-Length': String(buff.byteLength),
traceparent
},
body: buff
});
if (response.status >= 400) {
throw await convertResponseToError(response);
}
let bodyResp = '';
try {
bodyResp = await response.text();
return JSON.parse(bodyResp);
}
catch (err) {
throw new Error('Invalid JSON: ' + url + ', ' + bodyResp);
}
}
const response = await new Promise(async (resolve, reject) => {
const job = async () => {
try {
const response = await fetch(url, {
method,
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-type': `multipart/related; boundary=${boundary}`,
traceparent
},
body: await new Blob(body).arrayBuffer()
});
if (response.status >= 400) {
return reject(await convertResponseToError(response));
}
resolve(response);
}
catch (err) {
reject(err);
}
};
if (requestUrl.endsWith('drive/v3/files')) {
job.skipCounter = true;
}
if (process.env.ZIPKIN_URL) {
job.parentCtx = context.active();
}
quotaLimiter.addJob(job);
});
let bodyResp = '';
try {
bodyResp = await response.text();
return JSON.parse(bodyResp);
}
catch (err) {
throw new Error('Invalid JSON: ' + url + ', ' + bodyResp);
}
}