keeweb/app/scripts/storage/impl/storage-gdrive.js

265 lines
9.7 KiB
JavaScript
Raw Normal View History

2019-09-15 14:16:32 +02:00
import { StorageBase } from 'storage/storage-base';
import { Locale } from 'util/locale';
import { Features } from 'util/features';
import { GDriveApps } from 'const/cloud-storage-apps';
2016-03-13 17:08:25 +01:00
2017-01-31 07:50:28 +01:00
const NewFileIdPrefix = 'NewFile:';
2016-03-27 11:08:54 +02:00
// https://developers.google.com/identity/protocols/oauth2/web-server
2019-09-18 21:26:43 +02:00
class StorageGDrive extends StorageBase {
name = 'gdrive';
enabled = true;
uipos = 30;
2020-03-29 15:01:11 +02:00
iconSvg = 'google-drive';
2016-03-13 17:08:25 +01:00
2019-09-18 21:26:43 +02:00
_baseUrl = 'https://www.googleapis.com/drive/v3';
_baseUrlUpload = 'https://www.googleapis.com/upload/drive/v3';
2016-03-27 11:08:54 +02:00
2019-08-18 10:17:09 +02:00
getPathForName(fileName) {
2016-03-27 20:14:31 +02:00
return NewFileIdPrefix + fileName;
2019-09-18 21:26:43 +02:00
}
2016-03-27 20:14:31 +02:00
2019-08-18 10:17:09 +02:00
load(path, opts, callback) {
this.stat(path, opts, (err, stat) => {
2019-08-16 23:05:39 +02:00
if (err) {
return callback && callback(err);
}
this.logger.debug('Load', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
2019-08-16 23:05:39 +02:00
const url =
this._baseUrl +
2019-08-18 08:05:38 +02:00
'/files/{id}/revisions/{rev}?alt=media'
.replace('{id}', path)
.replace('{rev}', stat.rev);
this._xhr({
2019-08-18 10:17:09 +02:00
url,
2016-03-27 09:42:48 +02:00
responseType: 'arraybuffer',
2020-06-01 16:53:51 +02:00
success: (response) => {
this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts));
2016-03-27 09:42:48 +02:00
return callback && callback(null, response, { rev: stat.rev });
},
2020-06-01 16:53:51 +02:00
error: (err) => {
this.logger.error('Load error', path, err, this.logger.ts(ts));
2016-03-27 09:42:48 +02:00
return callback && callback(err);
2016-03-26 21:12:56 +01:00
}
});
});
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:08:25 +01:00
2019-08-18 10:17:09 +02:00
stat(path, opts, callback) {
2016-03-27 20:14:31 +02:00
if (path.lastIndexOf(NewFileIdPrefix, 0) === 0) {
return callback && callback({ notFound: true });
}
2020-06-01 16:53:51 +02:00
this._oauthAuthorize((err) => {
2016-03-26 21:12:56 +01:00
if (err) {
return callback && callback(err);
}
this.logger.debug('Stat', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
2019-08-16 23:05:39 +02:00
const url = this._baseUrl + '/files/{id}?fields=headRevisionId'.replace('{id}', path);
this._xhr({
2019-08-18 10:17:09 +02:00
url,
2016-03-27 09:42:48 +02:00
responseType: 'json',
2020-06-01 16:53:51 +02:00
success: (response) => {
2017-01-31 07:50:28 +01:00
const rev = response.headRevisionId;
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
2019-08-18 10:17:09 +02:00
return callback && callback(null, { rev });
2016-03-27 09:42:48 +02:00
},
2020-06-01 16:53:51 +02:00
error: (err) => {
this.logger.error('Stat error', this.logger.ts(ts), err);
2016-03-27 09:42:48 +02:00
return callback && callback(err);
}
2016-03-26 21:12:56 +01:00
});
});
2019-09-18 21:26:43 +02:00
}
2016-03-26 21:12:56 +01:00
2019-08-18 10:17:09 +02:00
save(path, opts, data, callback, rev) {
2020-06-01 16:53:51 +02:00
this._oauthAuthorize((err) => {
2016-08-21 17:43:59 +02:00
if (err) {
return callback && callback(err);
2016-03-27 20:14:31 +02:00
}
2016-08-21 17:43:59 +02:00
this.stat(path, opts, (err, stat) => {
if (rev) {
if (err) {
return callback && callback(err);
}
if (stat.rev !== rev) {
return callback && callback({ revConflict: true }, stat);
2016-03-27 09:42:48 +02:00
}
2016-03-26 21:12:56 +01:00
}
2016-08-21 17:43:59 +02:00
this.logger.debug('Save', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
const isNew = path.lastIndexOf(NewFileIdPrefix, 0) === 0;
let url;
let dataType;
let dataIsMultipart = false;
2016-08-21 17:43:59 +02:00
if (isNew) {
2019-08-18 08:05:38 +02:00
url =
this._baseUrlUpload +
'/files?uploadType=multipart&fields=id,headRevisionId';
2017-01-31 07:50:28 +01:00
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;
2016-08-21 17:43:59 +02:00
} else {
2019-08-16 23:05:39 +02:00
url =
this._baseUrlUpload +
'/files/{id}?uploadType=media&fields=headRevisionId'.replace('{id}', path);
2016-08-21 17:43:59 +02:00
}
this._xhr({
2019-08-18 10:17:09 +02:00
url,
2016-08-21 17:43:59 +02:00
method: isNew ? 'POST' : 'PATCH',
responseType: 'json',
2019-08-18 10:17:09 +02:00
data,
dataType,
dataIsMultipart,
2020-06-01 16:53:51 +02:00
success: (response) => {
2016-08-21 17:43:59 +02:00
this.logger.debug('Saved', path, this.logger.ts(ts));
2017-01-31 07:50:28 +01:00
const newRev = response.headRevisionId;
2016-08-21 17:43:59 +02:00
if (!newRev) {
return callback && callback('save error: no rev');
}
2019-08-18 08:05:38 +02:00
return (
callback &&
callback(null, { rev: newRev, path: isNew ? response.id : null })
);
2016-08-21 17:43:59 +02:00
},
2020-06-01 16:53:51 +02:00
error: (err) => {
2016-08-21 17:43:59 +02:00
this.logger.error('Save error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
2016-03-26 21:12:56 +01:00
});
});
2019-09-18 21:26:43 +02:00
}
2016-03-26 21:12:56 +01:00
2019-08-18 10:17:09 +02:00
list(dir, callback) {
2020-06-01 16:53:51 +02:00
this._oauthAuthorize((err) => {
2019-08-16 23:05:39 +02:00
if (err) {
return callback && callback(err);
}
this.logger.debug('List');
2019-08-18 08:05:38 +02:00
let query =
dir === 'shared'
? 'sharedWithMe=true'
: dir
? `"${dir}" in parents`
: '"root" in parents';
query += ' and trashed=false';
2019-08-16 23:05:39 +02:00
const url =
this._baseUrl +
'/files?fields={fields}&q={q}&pageSize=1000'
2019-08-18 08:05:38 +02:00
.replace(
'{fields}',
encodeURIComponent('files(id,name,mimeType,headRevisionId)')
)
2019-08-16 23:05:39 +02:00
.replace('{q}', encodeURIComponent(query));
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
this._xhr({
2019-08-18 10:17:09 +02:00
url,
2016-03-27 09:42:48 +02:00
responseType: 'json',
2020-06-01 16:53:51 +02:00
success: (response) => {
2016-03-27 09:42:48 +02:00
if (!response) {
this.logger.error('List error', this.logger.ts(ts));
2016-03-27 09:42:48 +02:00
return callback && callback('list error');
}
this.logger.debug('Listed', this.logger.ts(ts));
2020-06-01 16:53:51 +02:00
const fileList = response.files.map((f) => ({
2016-07-17 13:30:38 +02:00
name: f.name,
path: f.id,
2017-11-26 20:34:14 +01:00
rev: f.headRevisionId,
dir: f.mimeType === 'application/vnd.google-apps.folder'
2016-07-17 13:30:38 +02:00
}));
if (!dir) {
fileList.unshift({
name: Locale.gdriveSharedWithMe,
path: 'shared',
rev: undefined,
dir: true
});
}
2016-03-27 09:42:48 +02:00
return callback && callback(null, fileList);
},
2020-06-01 16:53:51 +02:00
error: (err) => {
this.logger.error('List error', this.logger.ts(ts), err);
2016-03-27 09:42:48 +02:00
return callback && callback(err);
2016-03-26 21:12:56 +01:00
}
});
});
2019-09-18 21:26:43 +02:00
}
2016-03-26 21:12:56 +01:00
2019-08-18 10:17:09 +02:00
remove(path, callback) {
this.logger.debug('Remove', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
const url = this._baseUrl + '/files/{id}'.replace('{id}', path);
this._xhr({
2019-08-18 10:17:09 +02:00
url,
2016-03-27 20:14:31 +02:00
method: 'DELETE',
responseType: 'json',
statuses: [200, 204],
success: () => {
this.logger.debug('Removed', path, this.logger.ts(ts));
2016-03-27 20:14:31 +02:00
return callback && callback();
},
2020-06-01 16:53:51 +02:00
error: (err) => {
this.logger.error('Remove error', path, err, this.logger.ts(ts));
2016-03-27 20:14:31 +02:00
return callback && callback(err);
}
});
2019-09-18 21:26:43 +02:00
}
2016-03-27 20:14:31 +02:00
2020-04-17 21:36:56 +02:00
logout() {
this._oauthRevokeToken('https://accounts.google.com/o/oauth2/revoke?token={token}');
2019-09-18 21:26:43 +02:00
}
2019-08-18 10:17:09 +02:00
_getOAuthConfig() {
2019-09-17 19:50:42 +02:00
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);
}
}
2016-03-27 15:18:05 +02:00
return {
2016-03-27 16:47:29 +02:00
scope: 'https://www.googleapis.com/auth/drive',
url: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
2019-08-18 10:17:09 +02:00
clientId,
clientSecret,
2016-03-27 14:57:22 +02:00
width: 600,
height: 400,
pkce: true,
redirectUrlParams: {
'access_type': 'offline'
}
2016-03-27 15:18:05 +02:00
};
2016-03-13 17:08:25 +01:00
}
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:08:25 +01:00
2019-09-15 14:16:32 +02:00
export { StorageGDrive };