mirror of https://github.com/keeweb/keeweb.git
318 lines
12 KiB
JavaScript
318 lines
12 KiB
JavaScript
import { StorageBase } from 'storage/storage-base';
|
|
import { Locale } from 'util/locale';
|
|
import { Features } from 'util/features';
|
|
import { UrlFormat } from 'util/formatting/url-format';
|
|
import { GDriveApps } from 'const/cloud-storage-apps';
|
|
|
|
const NewFileIdPrefix = 'NewFile:';
|
|
|
|
// https://developers.google.com/identity/protocols/oauth2/web-server
|
|
|
|
class StorageGDrive extends StorageBase {
|
|
name = 'gdrive';
|
|
enabled = true;
|
|
uipos = 30;
|
|
icon = 'google-drive';
|
|
|
|
_baseUrl = 'https://www.googleapis.com/drive/v3';
|
|
_baseUrlUpload = 'https://www.googleapis.com/upload/drive/v3';
|
|
|
|
getPathForName(fileName) {
|
|
return NewFileIdPrefix + fileName;
|
|
}
|
|
|
|
load(path, opts, callback) {
|
|
this.stat(path, opts, (err, stat) => {
|
|
if (err) {
|
|
return callback && callback(err);
|
|
}
|
|
this.logger.debug('Load', path);
|
|
const ts = this.logger.ts();
|
|
const url = UrlFormat.makeUrl(`${this._baseUrl}/files/${path}/revisions/${stat.rev}`, {
|
|
'alt': 'media'
|
|
});
|
|
this._xhr({
|
|
url,
|
|
responseType: 'arraybuffer',
|
|
success: (response) => {
|
|
this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts));
|
|
return callback && callback(null, response, { rev: stat.rev });
|
|
},
|
|
error: (err) => {
|
|
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
|
return callback && callback(err);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
stat(path, opts, callback) {
|
|
if (path.lastIndexOf(NewFileIdPrefix, 0) === 0) {
|
|
return callback && callback({ notFound: true });
|
|
}
|
|
this._oauthAuthorize((err) => {
|
|
if (err) {
|
|
return callback && callback(err);
|
|
}
|
|
this.logger.debug('Stat', path);
|
|
const ts = this.logger.ts();
|
|
const url = UrlFormat.makeUrl(`${this._baseUrl}/files/${path}`, {
|
|
fields: 'headRevisionId',
|
|
includeItemsFromAllDrives: true,
|
|
supportsAllDrives: true
|
|
});
|
|
this._xhr({
|
|
url,
|
|
responseType: 'json',
|
|
success: (response) => {
|
|
const rev = response.headRevisionId;
|
|
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
|
|
return callback && callback(null, { rev });
|
|
},
|
|
error: (err) => {
|
|
this.logger.error('Stat error', this.logger.ts(ts), err);
|
|
return callback && callback(err);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
save(path, opts, data, callback, rev) {
|
|
this._oauthAuthorize((err) => {
|
|
if (err) {
|
|
return callback && callback(err);
|
|
}
|
|
this.stat(path, opts, (err, stat) => {
|
|
if (rev) {
|
|
if (err) {
|
|
return callback && callback(err);
|
|
}
|
|
if (stat.rev !== rev) {
|
|
return callback && callback({ revConflict: true }, stat);
|
|
}
|
|
}
|
|
this.logger.debug('Save', path);
|
|
const ts = this.logger.ts();
|
|
const isNew = path.lastIndexOf(NewFileIdPrefix, 0) === 0;
|
|
let url;
|
|
let dataType;
|
|
let dataIsMultipart = false;
|
|
if (isNew) {
|
|
url = UrlFormat.makeUrl(`${this._baseUrlUpload}/files`, {
|
|
uploadType: 'multipart',
|
|
fields: 'id,headRevisionId',
|
|
includeItemsFromAllDrives: true,
|
|
supportsAllDrives: true
|
|
});
|
|
const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx';
|
|
const boundary = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
|
|
data = [
|
|
'--',
|
|
boundary,
|
|
'\r\n',
|
|
'Content-Type: application/json; charset=UTF-8',
|
|
'\r\n\r\n',
|
|
JSON.stringify({ name: fileName }),
|
|
'\r\n',
|
|
'--',
|
|
boundary,
|
|
'\r\n',
|
|
'Content-Type: application/octet-stream',
|
|
'\r\n\r\n',
|
|
data,
|
|
'\r\n',
|
|
'--',
|
|
boundary,
|
|
'--',
|
|
'\r\n'
|
|
];
|
|
dataType = 'multipart/related; boundary="' + boundary + '"';
|
|
dataIsMultipart = true;
|
|
} else {
|
|
url = UrlFormat.makeUrl(`${this._baseUrlUpload}/files/${path}`, {
|
|
uploadType: 'media',
|
|
fields: 'headRevisionId',
|
|
includeItemsFromAllDrives: true,
|
|
supportsAllDrives: true
|
|
});
|
|
}
|
|
this._xhr({
|
|
url,
|
|
method: isNew ? 'POST' : 'PATCH',
|
|
responseType: 'json',
|
|
data,
|
|
dataType,
|
|
dataIsMultipart,
|
|
success: (response) => {
|
|
this.logger.debug('Saved', path, this.logger.ts(ts));
|
|
const newRev = response.headRevisionId;
|
|
if (!newRev) {
|
|
return callback && callback('save error: no rev');
|
|
}
|
|
return (
|
|
callback &&
|
|
callback(null, { rev: newRev, path: isNew ? response.id : null })
|
|
);
|
|
},
|
|
error: (err) => {
|
|
this.logger.error('Save error', path, err, this.logger.ts(ts));
|
|
return callback && callback(err);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
list(dir, callback) {
|
|
this._oauthAuthorize((err) => {
|
|
if (err) {
|
|
return callback && callback(err);
|
|
}
|
|
this.logger.debug('List');
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
if (dir === 'drives') {
|
|
const urlParams = {
|
|
pageSize: 100
|
|
};
|
|
|
|
const url = UrlFormat.makeUrl(`${this._baseUrl}/drives`, urlParams);
|
|
|
|
this._xhr({
|
|
url,
|
|
responseType: 'json',
|
|
success: (response) => {
|
|
if (!response) {
|
|
this.logger.error('Drive list error', this.logger.ts(ts));
|
|
return callback?.('drive list error');
|
|
}
|
|
this.logger.debug('Listed drives', this.logger.ts(ts));
|
|
|
|
const fileList = response.drives.map((d) => ({
|
|
name: d.name,
|
|
path: d.id,
|
|
dir: true
|
|
}));
|
|
return callback?.(null, fileList);
|
|
},
|
|
error: (err) => {
|
|
this.logger.error('Drive dist error', this.logger.ts(ts), err);
|
|
return callback?.(err);
|
|
}
|
|
});
|
|
} else {
|
|
let query = 'trashed=false and ';
|
|
if (dir === 'shared') {
|
|
query += 'sharedWithMe=true';
|
|
} else if (dir) {
|
|
query += `"${dir}" in parents`;
|
|
} else {
|
|
query += '"root" in parents';
|
|
}
|
|
|
|
const urlParams = {
|
|
fields: 'files(id,name,mimeType,headRevisionId)',
|
|
q: query,
|
|
pageSize: 1000,
|
|
includeItemsFromAllDrives: true,
|
|
supportsAllDrives: true
|
|
};
|
|
|
|
const url = UrlFormat.makeUrl(`${this._baseUrl}/files`, urlParams);
|
|
|
|
this._xhr({
|
|
url,
|
|
responseType: 'json',
|
|
success: (response) => {
|
|
if (!response) {
|
|
this.logger.error('List error', this.logger.ts(ts));
|
|
return callback?.('list error');
|
|
}
|
|
this.logger.debug('Listed', this.logger.ts(ts));
|
|
|
|
const fileList = response.files.map((f) => ({
|
|
name: f.name,
|
|
path: f.id,
|
|
rev: f.headRevisionId,
|
|
dir: f.mimeType === 'application/vnd.google-apps.folder'
|
|
}));
|
|
if (!dir) {
|
|
fileList.unshift({
|
|
name: Locale.gdriveSharedWithMe,
|
|
path: 'shared',
|
|
rev: undefined,
|
|
dir: true
|
|
});
|
|
fileList.unshift({
|
|
name: Locale.gdriveTeamDrives,
|
|
path: 'drives',
|
|
rev: undefined,
|
|
dir: true
|
|
});
|
|
}
|
|
return callback?.(null, fileList);
|
|
},
|
|
error: (err) => {
|
|
this.logger.error('List error', this.logger.ts(ts), err);
|
|
return callback?.(err);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
remove(path, callback) {
|
|
this.logger.debug('Remove', path);
|
|
const ts = this.logger.ts();
|
|
const url = `${this._baseUrl}/files/${path}`;
|
|
this._xhr({
|
|
url,
|
|
method: 'DELETE',
|
|
responseType: 'json',
|
|
statuses: [200, 204],
|
|
success: () => {
|
|
this.logger.debug('Removed', path, this.logger.ts(ts));
|
|
return callback && callback();
|
|
},
|
|
error: (err) => {
|
|
this.logger.error('Remove error', path, err, this.logger.ts(ts));
|
|
return callback && callback(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
logout() {
|
|
this._oauthRevokeToken('https://accounts.google.com/o/oauth2/revoke?token={token}');
|
|
}
|
|
|
|
_getOAuthConfig() {
|
|
let clientId = this.appSettings.gdriveClientId;
|
|
let clientSecret = this.appSettings.gdriveClientSecret;
|
|
if (!clientId || !clientSecret) {
|
|
if (Features.isDesktop) {
|
|
({ id: clientId, secret: clientSecret } = GDriveApps.Desktop);
|
|
} else if (Features.isLocal) {
|
|
({ id: clientId, secret: clientSecret } = GDriveApps.Local);
|
|
} else {
|
|
({ id: clientId, secret: clientSecret } = GDriveApps.Production);
|
|
}
|
|
}
|
|
return {
|
|
scope: 'https://www.googleapis.com/auth/drive',
|
|
url: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
clientId,
|
|
clientSecret,
|
|
width: 600,
|
|
height: 400,
|
|
pkce: true,
|
|
redirectUrlParams: this.appSettings.shortLivedStorageToken
|
|
? {}
|
|
: { 'access_type': 'offline' }
|
|
};
|
|
}
|
|
}
|
|
|
|
export { StorageGDrive };
|