├── screenshot.png ├── src ├── assets │ ├── icon_128.png │ ├── icon_16.png │ └── icon_32.png ├── mount.html ├── manifest.json ├── mount.css ├── mount.js └── background.js └── README.md /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beaufortfrancois/cloud-storage-chrome-app/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/assets/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beaufortfrancois/cloud-storage-chrome-app/HEAD/src/assets/icon_128.png -------------------------------------------------------------------------------- /src/assets/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beaufortfrancois/cloud-storage-chrome-app/HEAD/src/assets/icon_16.png -------------------------------------------------------------------------------- /src/assets/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beaufortfrancois/cloud-storage-chrome-app/HEAD/src/assets/icon_32.png -------------------------------------------------------------------------------- /src/mount.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |
10 |
11 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Cloud Storage",
4 | "description": "Access Cloud Storage Buckets in the Files App",
5 | "version": "0.2",
6 | "minimum_chrome_version": "42",
7 |
8 | "icons": {
9 | "16": "assets/icon_16.png",
10 | "32": "assets/icon_32.png",
11 | "128": "assets/icon_128.png"
12 | },
13 | "permissions": [
14 | "fileSystemProvider",
15 | "identity",
16 | "storage"
17 | ],
18 | "file_system_provider_capabilities": {
19 | "configurable": false,
20 | "multiple_mounts": true,
21 | "source": "network"
22 | },
23 | "oauth2": {
24 | "client_id": "1019873396808-onql6al5ijtivb6ngmgboasifbg5r2hs.apps.googleusercontent.com",
25 | "scopes": [
26 | "https://www.googleapis.com/auth/devstorage.read_only"
27 | ]
28 | },
29 | "app": {
30 | "background": {
31 | "scripts": ["background.js"]
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/mount.css:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar {
2 | width: 0px !important;
3 | }
4 |
5 | body {
6 | margin: 0px;
7 | background-color: #4182fa;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | height: 200px;
12 | flex-direction: column;
13 | }
14 |
15 | #form {
16 | flex-direction: row;
17 | display: flex;
18 | }
19 |
20 | #lastBuckets {
21 | width: 300px;
22 | margin-top: 12px;
23 | max-height: 64px;
24 | overflow: overlay;
25 | }
26 |
27 | input, button {
28 | margin: 0 auto;
29 | display: block;
30 | border: 0;
31 | outline: none;
32 | }
33 |
34 | input {
35 | border-radius: 2px;
36 | width: 240px;
37 | line-height: 38px;
38 | border-bottom: 1px solid #555;
39 | padding-left: 12px;
40 | }
41 |
42 | span {
43 | display: block;
44 | margin-bottom: 8px;
45 | color: rgba(255, 255, 255, .7);
46 | }
47 |
48 | button {
49 | border-radius: 0 2px 2px 0;
50 | font-weight: bold;
51 | border-bottom: 1px solid #555;
52 | padding: 1px 12px;
53 | background-color: #444;
54 | color: white;
55 | transition: background-color .1s;
56 | text-shadow: 1px 1px 0 #333;
57 | }
58 |
59 | button:active {
60 | background-color: #888;
61 | }
62 |
--------------------------------------------------------------------------------
/src/mount.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input');
2 | var button = document.querySelector('button');
3 |
4 | function mount(bucket, callback) {
5 | var options = { fileSystemId: bucket, displayName: 'gs://' + bucket };
6 | chrome.fileSystemProvider.mount(options, function() {
7 | if (chrome.runtime.lastError) {
8 | console.error(chrome.runtime.lastError);
9 | }
10 | callback();
11 | });
12 | }
13 |
14 | function submit() {
15 | var bucket = input.value.trim();
16 | if (bucket.indexOf('gs://') === 0) {
17 | bucket = bucket.substr('gs://'.length);
18 | }
19 | if (!bucket) {
20 | return;
21 | }
22 | mount(bucket, function() {
23 | getLastBuckets(function(lastBuckets) {
24 | if (lastBuckets.length === 0 ||
25 | bucket !== lastBuckets[0]) {
26 | lastBuckets.unshift(bucket);
27 | }
28 | setLastBuckets(lastBuckets, function() {
29 | window.close();
30 | });
31 | });
32 | });
33 | }
34 |
35 | button.addEventListener('click', submit);
36 | input.addEventListener('keyup', function(event) {
37 | if (event.keyCode === 13) {
38 | submit();
39 | }
40 | });
41 |
42 | function submitLastBucket(event) {
43 | var index = 0;
44 | var bucket = 'gs://' + event.target.textContent;
45 | var type = function() {
46 | input.value = input.value + bucket[index];
47 | index++;
48 | if (index === bucket.length) {
49 | submit();
50 | } else {
51 | requestAnimationFrame(type);
52 | }
53 | }
54 | type();
55 | }
56 |
57 | function getLastBuckets(callback) {
58 | chrome.storage.local.get('lastBuckets', function(data) {
59 | var lastBuckets = data.lastBuckets || [];
60 | callback(lastBuckets);
61 | });
62 | }
63 |
64 | function setLastBuckets(lastBuckets, callback) {
65 | chrome.storage.local.set({'lastBuckets': lastBuckets}, callback);
66 | }
67 |
68 | getLastBuckets(function(lastBuckets) {
69 | lastBuckets.forEach(function(bucket) {
70 | var row = document.createElement('span');
71 | row.textContent = bucket;
72 | row.addEventListener('click', submitLastBucket);
73 | document.querySelector('#lastBuckets').appendChild(row);
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | // Helper function to get an authentication token.
2 | function getAuthToken(successCallback, errorCallback) {
3 | chrome.identity.getAuthToken({ 'interactive': true }, function(token) {
4 | if (chrome.runtime.lastError) {
5 | errorCallback();
6 | } else {
7 | successCallback(token);
8 | }
9 | });
10 | }
11 |
12 | // Helper function to send an authorized request and receive a JSON response.
13 | function request(url, successCallback, errorCallback) {
14 | getAuthToken(function(token) {
15 | var xhr = new XMLHttpRequest();
16 | xhr.open('GET', url);
17 | xhr.onloadend = function() {
18 | if (xhr.status === 200) {
19 | successCallback(xhr.response);
20 | } else if (xhr.status === 401) {
21 | // Removed cached token and try again.
22 | chrome.identity.removeCachedAuthToken({ token: token }, function() {
23 | request(url, successCallback, errorCallback);
24 | });
25 | } else {
26 | errorCallback();
27 | }
28 | }
29 | xhr.setRequestHeader('Authorization', 'Bearer ' + token);
30 | xhr.responseType = 'json';
31 | xhr.send();
32 | }, errorCallback);
33 | }
34 |
35 | // Helper function to sanitize metadata.
36 | function sanitizeMetadata(object, options) {
37 | function sanitize(metadata) {
38 | metadata.modificationTime = new Date(metadata.modificationTime);
39 | if (options) {
40 | if (!options.name) { delete metadata.name; }
41 | if (!options.thumbnail) { delete(metadata.thumbnail); }
42 | if (!options.size) { delete(metadata.size); }
43 | if (!options.mimeType) { delete(metadata.mimeType); }
44 | if (!options.modificationTime) { delete(metadata.modificationTime); }
45 | if (!options.isDirectory) { delete(metadata.isDirectory); }
46 | }
47 | return metadata;
48 | }
49 | if (object instanceof Array) {
50 | return object.map(sanitize)
51 | } else {
52 | return sanitize(object);
53 | }
54 |
55 | }
56 |
57 | // Get cloud storage object media link URL.
58 | function getObjectMediaLink(bucket, object, successCallback, errorCallback) {
59 | var url = 'https://www.googleapis.com/storage/v1/b/' + bucket +
60 | '/o/' + encodeURIComponent(object) + '?fields=mediaLink';
61 | request(url, successCallback, errorCallback);
62 | }
63 |
64 | // Get cloud storage object list that starts with a prefix.
65 | function getObjectsList(bucket, prefix, successCallback, errorCallback) {
66 | var url = 'https://www.googleapis.com/storage/v1/b/' + bucket + '/o/' +
67 | '?delimiter=%2F' +
68 | '&fields=' + encodeURIComponent('items(name,size,updated,contentType),prefixes') +
69 | '&prefix=' + (prefix ? encodeURIComponent(prefix) : '');
70 | request(url, successCallback, errorCallback);
71 | }
72 |
73 | function onGetMetadataRequested(options, onSuccess, onError) {
74 | console.log('onGetMetadataRequested', options.entryPath);
75 |
76 | if (options.entryPath === '/') {
77 | // Return static root entry.
78 | var root = {isDirectory: true, name: '', size: 0, modificationTime: new Date()};
79 | onSuccess(sanitizeMetadata(root, options));
80 | return;
81 | }
82 |
83 | var bucket = options.fileSystemId;
84 | var prefix = options.entryPath.substr(1); // Removes starting slash.
85 | getObjectsList(bucket, prefix, function(response) {
86 | var entry = null;
87 | if (response.items) {
88 | for (var item of response.items) {
89 | if (item.name === prefix) {
90 | var entryName = item.name;
91 | if (entryName.lastIndexOf('/') >= 0) {
92 | entryName = entryName.substring(entryName.lastIndexOf('/')+1);
93 | }
94 | // Return a file entry.
95 | entry = {
96 | 'isDirectory': false,
97 | 'name': entryName,
98 | 'size': parseInt(item.size, 10),
99 | 'modificationTime': new Date(item.updated),
100 | 'mimeType': item.contentType
101 | };
102 | break;
103 | }
104 | }
105 | } else if (response.prefixes) {
106 | // Return a directory entry.
107 | entry = {
108 | 'isDirectory': true,
109 | 'name': prefix,
110 | 'size': 0,
111 | 'modificationTime': new Date()
112 | };
113 | }
114 | if (entry) {
115 | onSuccess(sanitizeMetadata(entry, options));
116 | } else {
117 | onError('NOT_FOUND');
118 | }
119 | }, function() {
120 | onError('FAILED');
121 | });
122 | }
123 |
124 | function onReadDirectoryRequested(options, onSuccess, onError) {
125 | console.log('onReadDirectoryRequested', options.directoryPath);
126 |
127 | var bucket = options.fileSystemId;
128 | var prefix = '';
129 | if (options.directoryPath !== '/') {
130 | prefix = options.directoryPath.substr(1) + '/';
131 | }
132 | getObjectsList(bucket, prefix, function(response) {
133 | var entries = [];
134 | if (response.items) {
135 | // Add all files in this directory.
136 | for (var item of response.items) {
137 | if (item.name === prefix) {
138 | // Skip folder...
139 | continue;
140 | }
141 | entries.push({
142 | 'isDirectory': false,
143 | 'name': item.name.substr(prefix.length),
144 | 'size': parseInt(item.size, 10),
145 | 'modificationTime': new Date(item.updated),
146 | 'mimeType': item.contentType
147 | });
148 | }
149 | }
150 | if (response.prefixes) {
151 | // Add all directories in this directory.
152 | for (var item of response.prefixes) {
153 | entries.push({
154 | 'isDirectory': true,
155 | 'name': item.substr(prefix.length).slice(0, -1),
156 | 'size': 0,
157 | 'modificationTime': new Date()
158 | });
159 | }
160 | }
161 | onSuccess(sanitizeMetadata(entries, options), false /* last call */);
162 | }, function() {
163 | onError('FAILED');
164 | });
165 | }
166 |
167 | // A map with currently opened files. As key it has requestId of
168 | // openFileRequested and as a value the file path.
169 | var openedFiles = {};
170 |
171 | function onOpenFileRequested(options, onSuccess, onError) {
172 | console.log('onOpenFileRequested', options);
173 | if (options.mode != 'READ' || options.create) {
174 | onError('INVALID_OPERATION');
175 | } else {
176 | openedFiles[options.requestId] = options.filePath;
177 | onSuccess();
178 | }
179 | }
180 |
181 | function onReadFileRequested(options, onSuccess, onError) {
182 | console.log('onReadFileRequested', options);
183 |
184 | var bucket = options.fileSystemId;
185 | var filePath = openedFiles[options.openRequestId].substr(1);
186 | getObjectMediaLink(bucket, filePath, function(response) {
187 | getAuthToken(function(token) {
188 | var xhr = new XMLHttpRequest();
189 | xhr.open('GET', response.mediaLink);
190 | xhr.responseType = 'arraybuffer';
191 | xhr.setRequestHeader('Authorization', 'Bearer ' + token);
192 | xhr.setRequestHeader('Range', 'bytes=' + options.offset + '-' +
193 | (options.length + options.offset - 1));
194 | xhr.onloadend = function() {
195 | if (xhr.status === 206) {
196 | onSuccess(xhr.response, false /* last call */);
197 | } else if (xhr.status === 416) {
198 | // There's nothing more...
199 | onSuccess(new ArrayBuffer(), false /* last call */);
200 | } else {
201 | onError('NOT_FOUND');
202 | }
203 | };
204 | xhr.send();
205 | }, function() {
206 | onError('ACCESS_DENIED');
207 | });
208 | }, function() {
209 | onError('NOT_FOUND');
210 | });
211 | }
212 |
213 | function onCloseFileRequested(options, onSuccess, onError) {
214 | console.log('onCloseFileRequested', options);
215 | if (!openedFiles[options.openRequestId]) {
216 | onError('INVALID_OPERATION');
217 | return;
218 | }
219 |
220 | delete openedFiles[options.openRequestId];
221 | onSuccess();
222 | }
223 |
224 | function onUnmountRequested(options, onSuccess, onError) {
225 | console.log('onUnmountRequested', options);
226 | onSuccess();
227 | chrome.fileSystemProvider.unmount({ fileSystemId: options.fileSystemId });
228 | }
229 |
230 | function onMountRequested(onSuccess, onError) {
231 | console.log('onMountRequested');
232 | onSuccess();
233 | showMountWindow();
234 | }
235 |
236 | function showMountWindow() {
237 | chrome.app.window.create('mount.html', {
238 | id: 'mount-window',
239 | resizable: false,
240 | frame: { color: "#4182fa" },
241 | innerBounds: { width: 420, height: 200, }
242 | });
243 | }
244 |
245 | chrome.fileSystemProvider.onGetMetadataRequested.addListener(onGetMetadataRequested);
246 | chrome.fileSystemProvider.onReadDirectoryRequested.addListener(onReadDirectoryRequested);
247 | chrome.fileSystemProvider.onOpenFileRequested.addListener(onOpenFileRequested);
248 | chrome.fileSystemProvider.onReadFileRequested.addListener(onReadFileRequested);
249 | chrome.fileSystemProvider.onCloseFileRequested.addListener(onCloseFileRequested);
250 | chrome.fileSystemProvider.onUnmountRequested.addListener(onUnmountRequested);
251 | chrome.fileSystemProvider.onMountRequested && chrome.fileSystemProvider.onMountRequested.addListener(onMountRequested);
252 |
253 | chrome.app.runtime.onLaunched.addListener(showMountWindow);
254 |
--------------------------------------------------------------------------------