/app/scripts/storage/impl/storage-onedrive.js

https://github.com/keeweb/keeweb · JavaScript · 244 lines · 230 code · 13 blank · 1 comment · 42 complexity · 06e936c3c38b7c4021a021da17a7dfca MD5 · raw file

  1. import { StorageBase } from 'storage/storage-base';
  2. import { OneDriveApps } from 'const/cloud-storage-apps';
  3. import { Features } from 'util/features';
  4. // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
  5. class StorageOneDrive extends StorageBase {
  6. name = 'onedrive';
  7. enabled = true;
  8. uipos = 40;
  9. iconSvg = 'onedrive';
  10. _baseUrl = 'https://graph.microsoft.com/v1.0/me';
  11. getPathForName(fileName) {
  12. return '/drive/root:/' + fileName + '.kdbx';
  13. }
  14. load(path, opts, callback) {
  15. this._oauthAuthorize((err) => {
  16. if (err) {
  17. return callback && callback(err);
  18. }
  19. this.logger.debug('Load', path);
  20. const ts = this.logger.ts();
  21. const url = this._baseUrl + path;
  22. this._xhr({
  23. url,
  24. responseType: 'json',
  25. success: (response) => {
  26. const downloadUrl = response['@microsoft.graph.downloadUrl'];
  27. let rev = response.eTag;
  28. if (!downloadUrl || !response.eTag) {
  29. this.logger.debug(
  30. 'Load error',
  31. path,
  32. 'no download url',
  33. response,
  34. this.logger.ts(ts)
  35. );
  36. return callback && callback('no download url');
  37. }
  38. this._xhr({
  39. url: downloadUrl,
  40. responseType: 'arraybuffer',
  41. skipAuth: true,
  42. success: (response, xhr) => {
  43. rev = xhr.getResponseHeader('ETag') || rev;
  44. this.logger.debug('Loaded', path, rev, this.logger.ts(ts));
  45. return callback && callback(null, response, { rev });
  46. },
  47. error: (err) => {
  48. this.logger.error('Load error', path, err, this.logger.ts(ts));
  49. return callback && callback(err);
  50. }
  51. });
  52. },
  53. error: (err) => {
  54. this.logger.error('Load error', path, err, this.logger.ts(ts));
  55. return callback && callback(err);
  56. }
  57. });
  58. });
  59. }
  60. stat(path, opts, callback) {
  61. this._oauthAuthorize((err) => {
  62. if (err) {
  63. return callback && callback(err);
  64. }
  65. this.logger.debug('Stat', path);
  66. const ts = this.logger.ts();
  67. const url = this._baseUrl + path;
  68. this._xhr({
  69. url,
  70. responseType: 'json',
  71. success: (response) => {
  72. const rev = response.eTag;
  73. if (!rev) {
  74. this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts));
  75. return callback && callback('no eTag');
  76. }
  77. this.logger.debug('Stated', path, rev, this.logger.ts(ts));
  78. return callback && callback(null, { rev });
  79. },
  80. error: (err, xhr) => {
  81. if (xhr.status === 404) {
  82. this.logger.debug('Stated not found', path, this.logger.ts(ts));
  83. return callback && callback({ notFound: true });
  84. }
  85. this.logger.error('Stat error', path, err, this.logger.ts(ts));
  86. return callback && callback(err);
  87. }
  88. });
  89. });
  90. }
  91. save(path, opts, data, callback, rev) {
  92. this._oauthAuthorize((err) => {
  93. if (err) {
  94. return callback && callback(err);
  95. }
  96. this.logger.debug('Save', path, rev);
  97. const ts = this.logger.ts();
  98. const url = this._baseUrl + path + ':/content';
  99. this._xhr({
  100. url,
  101. method: 'PUT',
  102. responseType: 'json',
  103. headers: rev ? { 'If-Match': rev } : null,
  104. data,
  105. statuses: [200, 201, 412],
  106. success: (response, xhr) => {
  107. rev = response.eTag;
  108. if (!rev) {
  109. this.logger.error('Save error', path, 'no eTag', this.logger.ts(ts));
  110. return callback && callback('no eTag');
  111. }
  112. if (xhr.status === 412) {
  113. this.logger.debug('Save conflict', path, rev, this.logger.ts(ts));
  114. return callback && callback({ revConflict: true }, { rev });
  115. }
  116. this.logger.debug('Saved', path, rev, this.logger.ts(ts));
  117. return callback && callback(null, { rev });
  118. },
  119. error: (err) => {
  120. this.logger.error('Save error', path, err, this.logger.ts(ts));
  121. return callback && callback(err);
  122. }
  123. });
  124. });
  125. }
  126. list(dir, callback) {
  127. this._oauthAuthorize((err) => {
  128. if (err) {
  129. return callback && callback(err);
  130. }
  131. this.logger.debug('List');
  132. const ts = this.logger.ts();
  133. const url = this._baseUrl + (dir ? `${dir}:/children` : '/drive/root/children');
  134. this._xhr({
  135. url,
  136. responseType: 'json',
  137. success: (response) => {
  138. if (!response || !response.value) {
  139. this.logger.error('List error', this.logger.ts(ts), response);
  140. return callback && callback('list error');
  141. }
  142. this.logger.debug('Listed', this.logger.ts(ts));
  143. const fileList = response.value
  144. .filter((f) => f.name)
  145. .map((f) => ({
  146. name: f.name,
  147. path: f.parentReference.path + '/' + f.name,
  148. rev: f.eTag,
  149. dir: !!f.folder
  150. }));
  151. return callback && callback(null, fileList);
  152. },
  153. error: (err) => {
  154. this.logger.error('List error', this.logger.ts(ts), err);
  155. return callback && callback(err);
  156. }
  157. });
  158. });
  159. }
  160. remove(path, callback) {
  161. this.logger.debug('Remove', path);
  162. const ts = this.logger.ts();
  163. const url = this._baseUrl + path;
  164. this._xhr({
  165. url,
  166. method: 'DELETE',
  167. responseType: 'json',
  168. statuses: [200, 204],
  169. success: () => {
  170. this.logger.debug('Removed', path, this.logger.ts(ts));
  171. return callback && callback();
  172. },
  173. error: (err) => {
  174. this.logger.error('Remove error', path, err, this.logger.ts(ts));
  175. return callback && callback(err);
  176. }
  177. });
  178. }
  179. mkdir(path, callback) {
  180. this._oauthAuthorize((err) => {
  181. if (err) {
  182. return callback && callback(err);
  183. }
  184. this.logger.debug('Make dir', path);
  185. const ts = this.logger.ts();
  186. const url = this._baseUrl + '/drive/root/children';
  187. const data = JSON.stringify({ name: path.replace('/drive/root:/', ''), folder: {} });
  188. this._xhr({
  189. url,
  190. method: 'POST',
  191. responseType: 'json',
  192. statuses: [200, 204],
  193. data,
  194. dataType: 'application/json',
  195. success: () => {
  196. this.logger.debug('Made dir', path, this.logger.ts(ts));
  197. return callback && callback();
  198. },
  199. error: (err) => {
  200. this.logger.error('Make dir error', path, err, this.logger.ts(ts));
  201. return callback && callback(err);
  202. }
  203. });
  204. });
  205. }
  206. logout(enabled) {
  207. this._oauthRevokeToken();
  208. }
  209. _getOAuthConfig() {
  210. let clientId = this.appSettings.onedriveClientId;
  211. let clientSecret = this.appSettings.onedriveClientSecret;
  212. if (!clientId || !clientSecret) {
  213. if (Features.isLocal) {
  214. ({ id: clientId, secret: clientSecret } = OneDriveApps.Local);
  215. } else {
  216. ({ id: clientId, secret: clientSecret } = OneDriveApps.Production);
  217. }
  218. }
  219. return {
  220. url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
  221. tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
  222. scope: 'files.readwrite offline_access',
  223. clientId,
  224. clientSecret,
  225. pkce: true,
  226. width: 600,
  227. height: 500
  228. };
  229. }
  230. }
  231. export { StorageOneDrive };