Skip to content

Commit fd4a3d6

Browse files
committed
New: Added Google Drive export.
1 parent a5a8362 commit fd4a3d6

File tree

6 files changed

+418
-4
lines changed

6 files changed

+418
-4
lines changed

main.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {ArcWindowsManager} = require('./scripts/windows-manager');
66
const {UpdateStatus} = require('./scripts/update-status');
77
const {ArcMainMenu} = require('./scripts/main-menu');
88
const {ArcIdentity} = require('./scripts/oauth2');
9+
const {DriveExport} = require('./scripts/drive-export');
910

1011
class Arc {
1112
constructor() {
@@ -154,3 +155,57 @@ ipc.on('check-for-update', () => {
154155
ipc.on('install-update', () => {
155156
arcApp.us.installUpdate();
156157
});
158+
159+
ipc.on('google-drive-data-save', (event, requestId, content, type, fileName) => {
160+
var config = {
161+
resource: {
162+
name: fileName,
163+
description: 'Advanced REST client data export file.'
164+
},
165+
media: {
166+
mimeType: type || 'application/json',
167+
body: content
168+
}
169+
};
170+
const drive = new DriveExport();
171+
drive.create(config)
172+
.then(result => {
173+
event.sender.send('google-drive-data-save-result', requestId, result);
174+
})
175+
.catch(cause => {
176+
event.sender.send('google-drive-data-save-error', requestId, cause);
177+
});
178+
});
179+
180+
ipc.on('drive-request-save', (event, requestId, request, fileName) => {
181+
var driveId;
182+
if (request.driveId) {
183+
driveId = request.driveId;
184+
delete request.driveId;
185+
}
186+
var config = {
187+
resource: {
188+
name: fileName + '.arc',
189+
},
190+
media: {
191+
mimeType: 'application/json',
192+
body: request
193+
}
194+
};
195+
const drive = new DriveExport();
196+
var promise;
197+
if (driveId) {
198+
promise = drive.update(driveId, config);
199+
} else {
200+
config.resource.description = request.description || 'Advanced REST client export file.';
201+
promise = drive.create(config);
202+
}
203+
204+
promise
205+
.then(result => {
206+
event.sender.send('drive-request-save-result', requestId, result);
207+
})
208+
.catch(cause => {
209+
event.sender.send('drive-request-save-error', requestId, cause);
210+
});
211+
});

package-lock.json

Lines changed: 24 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"electron-updater": "^2.10.1",
2929
"form-data": "^2.3.1",
3030
"fs-extra": "^4.0.2",
31+
"node-fetch": "^1.7.3",
3132
"node-gyp": "^3.6.2",
3233
"node-pre-gyp": "^0.6.38",
3334
"ntlm": "^0.1.3"

scripts/drive-export.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* @copyright Copyright 2017 Pawel Psztyc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
******************************************************************************/
16+
17+
const {ArcIdentity} = require('./oauth2');
18+
const fs = require('fs-extra');
19+
const path = require('path');
20+
const fetch = require('node-fetch');
21+
const URLSafeBase64 = require('urlsafe-base64');
22+
23+
class DriveExport {
24+
25+
constructor() {
26+
/**
27+
* File meta boundary for POST calls.
28+
*/
29+
this.boundary = 'ARCFormBoundary49nr1hyovoq1tt9';
30+
this.delimiter = '\r\n--' + this.boundary + '\r\n';
31+
this.closeDelimiter = '\r\n--' + this.boundary + '--';
32+
/**
33+
* Drive's registered content type.
34+
* It will be used to search for app's files in the Drive.
35+
* Drive's handlers will recognize the app and will run it from Drive UI.
36+
*/
37+
this.mime = 'application/restclient+data';
38+
/**
39+
* Extension name for files stored in Google Drive.
40+
*/
41+
this.extension = 'arc';
42+
/**
43+
* A list of allowed resources (file metadata) in the file create request.
44+
* See https://developers.google.com/drive/v3/reference/files/create for full list.
45+
*/
46+
this.allowedResource = [
47+
'appProperties', 'contentHints', 'createdTime', 'description', 'folderColorRgb', 'id',
48+
'mimeType', 'modifiedTime', 'name', 'parents', 'properties', 'starred', 'viewedByMeTime',
49+
'viewersCanCopyContent', 'writersCanShare'
50+
];
51+
}
52+
53+
auth() {
54+
return ArcIdentity.getAuthToken({interactive: true});
55+
}
56+
/**
57+
* Creates a Google Drive File.
58+
*
59+
* If `config.resource.mimeType` is not set and `drive.file.mime` is set then
60+
* `this.mime` is used instead.
61+
*
62+
* This script will automatically set file thumbnail if not set
63+
* (`config.resource.contentHints.thumbnail` object value).
64+
*
65+
* @param {Object} config A file creation configuration consisted with:
66+
* - {Object} `resource` - A file metadata. See `this.allowedResource` for
67+
* more information.
68+
* - {Object} `media` - A file contents definition to save on drive.
69+
* It must have defined following keys:
70+
* - {String} `mimeType` - A media mime type
71+
* - {String|Object} `body` - A content to save.
72+
*/
73+
create(config) {
74+
try {
75+
config = this.ensureDriveFileConfig(config);
76+
} catch (e) {
77+
return Promise.reject(e);
78+
}
79+
80+
var promise = Promise.resolve();
81+
// var res = config.resource;
82+
// if (!res.contentHints || !res.contentHints.thumbnail || !res.contentHints.image) {
83+
// promise = this._appSafeIcon()
84+
// .then(icon => {
85+
// if (!config.resource.contentHints) {
86+
// config.resource.contentHints = {};
87+
// }
88+
// config.resource.contentHints.thumbnail = {
89+
// image: icon,
90+
// mimeType: 'image/png'
91+
// };
92+
// });
93+
// } else {
94+
// promise = Promise.resolve();
95+
// }
96+
return promise
97+
.then(() => this.auth())
98+
.then(at => this._uploadFile(at, config));
99+
}
100+
/**
101+
* Update a file on Google Drive.
102+
*
103+
* @param {String} fileId A Google Drive file ID.
104+
* @param {Object} config The same as for `create` function.
105+
* @return {Promise} Fulfilled promise with file properties (the response).
106+
*/
107+
update(fileId, config) {
108+
try {
109+
config = this.ensureDriveFileConfig(config);
110+
} catch (e) {
111+
return Promise.reject(e);
112+
}
113+
return this.auth()
114+
.then(at => this._uploadUpdate(fileId, at, config));
115+
}
116+
/**
117+
* Ensure that the file has correct configuration and throw an error if not.
118+
* Also it will add a mime type of the file if not present.
119+
*/
120+
ensureDriveFileConfig(config) {
121+
if (!config) {
122+
throw new Error('Config argument is not specified.');
123+
}
124+
if (!config.resource || !config.media) {
125+
throw new Error('Invalid arguments.');
126+
}
127+
var invalidArguments = Object.keys(config.resource).filter(key => {
128+
return this.allowedResource.indexOf(key) === -1;
129+
});
130+
if (invalidArguments.length) {
131+
throw new Error('Unknown argument for resource: ' + invalidArguments.join(', '));
132+
}
133+
if (!config.resource.mimeType && this.mime) {
134+
config.resource.mimeType = this.mime;
135+
}
136+
return config;
137+
}
138+
/**
139+
* Creates an app icon string acceptable by Drive API
140+
*/
141+
_appSafeIcon() {
142+
const file = path.join(__dirname, '..', 'assets', 'icon.iconset', 'icon_128x128.png');
143+
return fs.readFile(file)
144+
.then(buffer => {
145+
let prefix = 'data:image/png;base64,';
146+
147+
let image = prefix + buffer.toString('base64');
148+
image = image.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
149+
return image;
150+
});
151+
}
152+
/**
153+
* Uploads the file.
154+
* @param {String} accessToken Google Access token
155+
* @param {Object} options Resource to upload
156+
*/
157+
_uploadFile(accessToken, options) {
158+
var init = {
159+
method: 'POST',
160+
body: this._getPayload(options),
161+
headers: this._getUploadHeaders(accessToken),
162+
};
163+
return fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', init)
164+
.then((response) => {
165+
if (!response.ok) {
166+
return Promise.reject(response.json());
167+
}
168+
return response.json();
169+
});
170+
}
171+
172+
_uploadUpdate(fileId, accessToken, options) {
173+
var init = {
174+
method: 'PATCH',
175+
body: this._getPayload(options),
176+
headers: this._getUploadHeaders(accessToken),
177+
};
178+
var url = `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`;
179+
return fetch(url, init)
180+
.then(response => {
181+
if (!response.ok) {
182+
return Promise.reject(response.json());
183+
}
184+
return response.json();
185+
});
186+
}
187+
188+
_getUploadHeaders(accessToken) {
189+
var headers = {
190+
'Authorization': 'Bearer ' + accessToken,
191+
'Content-Type': 'multipart/related; boundary="' + this.boundary + '"'
192+
};
193+
return headers;
194+
}
195+
196+
_getPayload(config) {
197+
var content;
198+
if (typeof config.media.body !== 'string') {
199+
content = JSON.stringify(config.media.body);
200+
} else {
201+
content = config.media.body;
202+
}
203+
var d = this.delimiter;
204+
var cd = this.closeDelimiter;
205+
let meta = JSON.stringify(config.resource);
206+
207+
let body = `${d}Content-Type: application/json; charset=UTF-8\r\n\r\n${meta}`;
208+
body += `${d}Content-Type: ${config.media.mimeType}\r\n\r\n`;
209+
body += `${content}${cd}`;
210+
return body;
211+
}
212+
}
213+
exports.DriveExport = DriveExport;

src/arc-electron.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<link rel="import" href="arc-request-panel/arc-request-panel.html">
4343
<link rel="import" href="file-export/file-export.html">
4444
<link rel="import" href="about-arc/about-arc.html">
45+
<link rel="import" href="electron-drive/electron-drive.html">
4546

4647
<dom-module id="arc-electron">
4748
<template>
@@ -202,6 +203,7 @@
202203
<project-model></project-model>
203204
<!-- Electron specific -->
204205
<file-export></file-export>
206+
<electron-drive></electron-drive>
205207
</template>
206208
<script>
207209
const {ArcPreferences} = require('./scripts/arc-preferences');

0 commit comments

Comments
 (0)