@ibm-aspera/sdk 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.editorconfig +13 -0
  2. package/.eslintrc.js +128 -0
  3. package/.github/CODE_OF_CONDUCT.md +128 -0
  4. package/.github/CONTRIBUTING.md +147 -0
  5. package/.github/workflows/ci.yml +36 -0
  6. package/.github/workflows/documentation.yml +43 -0
  7. package/.github/workflows/npm_upload.yml +30 -0
  8. package/.husky/pre-commit +4 -0
  9. package/CHANGELOG.md +124 -0
  10. package/LICENSE +201 -0
  11. package/README.md +25 -0
  12. package/dist/commonjs/app/core.d.ts +219 -0
  13. package/dist/commonjs/app/core.js +546 -0
  14. package/dist/commonjs/app/installer.d.ts +9 -0
  15. package/dist/commonjs/app/installer.js +50 -0
  16. package/dist/commonjs/constants/constants.d.ts +6 -0
  17. package/dist/commonjs/constants/constants.js +9 -0
  18. package/dist/commonjs/constants/messages.d.ts +29 -0
  19. package/dist/commonjs/constants/messages.js +32 -0
  20. package/dist/commonjs/helpers/client/client.d.ts +5 -0
  21. package/dist/commonjs/helpers/client/client.js +7 -0
  22. package/dist/commonjs/helpers/client/http-client.d.ts +42 -0
  23. package/dist/commonjs/helpers/client/http-client.js +84 -0
  24. package/dist/commonjs/helpers/client/safari-client.d.ts +99 -0
  25. package/dist/commonjs/helpers/client/safari-client.js +252 -0
  26. package/dist/commonjs/helpers/helpers.d.ts +84 -0
  27. package/dist/commonjs/helpers/helpers.js +197 -0
  28. package/dist/commonjs/helpers/http.d.ts +16 -0
  29. package/dist/commonjs/helpers/http.js +42 -0
  30. package/dist/commonjs/helpers/ws.d.ts +62 -0
  31. package/dist/commonjs/helpers/ws.js +182 -0
  32. package/dist/commonjs/index.d.ts +41 -0
  33. package/dist/commonjs/index.js +99 -0
  34. package/dist/commonjs/models/aspera-sdk.model.d.ts +213 -0
  35. package/dist/commonjs/models/aspera-sdk.model.js +288 -0
  36. package/dist/commonjs/models/models.d.ts +640 -0
  37. package/dist/commonjs/models/models.js +2 -0
  38. package/dist/js/aspera-sdk.js +3 -0
  39. package/dist/js/aspera-sdk.js.LICENSE.txt +7 -0
  40. package/dist/js/aspera-sdk.js.map +1 -0
  41. package/docs/DEVELOPMENT.md +38 -0
  42. package/jest.config.js +15 -0
  43. package/jest.setup.js +0 -0
  44. package/package.json +50 -0
  45. package/src/app/core.ts +610 -0
  46. package/src/app/installer.ts +53 -0
  47. package/src/constants/constants.ts +16 -0
  48. package/src/constants/messages.ts +29 -0
  49. package/src/helpers/client/client.ts +11 -0
  50. package/src/helpers/client/http-client.ts +92 -0
  51. package/src/helpers/client/safari-client.ts +318 -0
  52. package/src/helpers/helpers.ts +200 -0
  53. package/src/helpers/http.ts +39 -0
  54. package/src/helpers/ws.ts +215 -0
  55. package/src/index.html +404 -0
  56. package/src/index.ts +104 -0
  57. package/src/models/aspera-sdk.model.ts +360 -0
  58. package/src/models/models.ts +669 -0
  59. package/tests/client.spec.ts +52 -0
  60. package/tests/core.spec.ts +13 -0
  61. package/tests/helpers.spec.ts +124 -0
  62. package/tests/http.spec.ts +14 -0
  63. package/tests/installer.spec.ts +135 -0
  64. package/tests/mocks.ts +11 -0
  65. package/tsconfig.json +10 -0
  66. package/tsconfig.module.json +15 -0
  67. package/typedoc.js +17 -0
  68. package/webpack.config.js +53 -0
@@ -0,0 +1,215 @@
1
+ import {errorLog, generatePromiseObjects, getWebsocketUrl} from './helpers';
2
+ import {messages} from '../constants/messages';
3
+ import {asperaSdk} from '../index';
4
+ import {TransferResponse} from '../models/aspera-sdk.model';
5
+ import {WebsocketEvent, WebsocketMessage, WebsocketTopics} from '../models/models';
6
+
7
+ export class WebsocketService {
8
+ /** The main websocket connection to Aspera App*/
9
+ private globalSocket: WebSocket;
10
+ /** A map of requested subscription names and the callback for them */
11
+ private sockets: Map<WebsocketTopics, Function> = new Map();
12
+ /** The callback for websocket events */
13
+ private eventListener: Function;
14
+ /** Indicator if the websocket is already connected */
15
+ private isConnected = false;
16
+ /** Global promise object that resolves when init completes */
17
+ private initPromise = generatePromiseObjects();
18
+
19
+ /** Log call for not being ready */
20
+ private handleNotReady(): void {
21
+ errorLog(messages.websocketNotReady);
22
+ }
23
+
24
+ /**
25
+ * This function handles when a connection is opened
26
+ */
27
+ private handleOpen = (): void => {
28
+ if (this.isConnected || !this.joinChannel()) {
29
+ return;
30
+ }
31
+
32
+ this.isConnected = true;
33
+ this.updateRpcPort();
34
+ this.notifyEvent('RECONNECT');
35
+ };
36
+
37
+ /**
38
+ * This function handles completed subscription
39
+ */
40
+ private handleClose = (): void => {
41
+ if (this.isConnected) {
42
+ this.isConnected = false;
43
+ this.notifyEvent('CLOSED');
44
+ }
45
+
46
+ if (!this.globalSocket) {
47
+ this.handleNotReady();
48
+ return;
49
+ }
50
+
51
+ this.reconnect();
52
+ };
53
+
54
+ /**
55
+ * This function handles errors received from the websocket
56
+ */
57
+ private handleError = (): void => {
58
+ errorLog(messages.websocketClosedError);
59
+ };
60
+
61
+ /**
62
+ * This function handles messages received from the websocket
63
+ */
64
+ private handleMessage = (message: MessageEvent<string>): void => {
65
+ let data: WebsocketMessage|undefined;
66
+
67
+ try {
68
+ data = JSON.parse(message.data);
69
+ } catch (error) {
70
+ errorLog('Unable to parse Websocket message', {error, message});
71
+ }
72
+
73
+ // Message we get on subscription
74
+ if (data && data.id === 1) {
75
+ this.initPromise.resolver(data);
76
+
77
+ return;
78
+ }
79
+
80
+ const socket = this.sockets.get(data.method);
81
+
82
+ if (socket && data.params) {
83
+ socket(data.params);
84
+ }
85
+ };
86
+
87
+ /**
88
+ * This function joins the channel to be able to subscribe to events
89
+ */
90
+ private joinChannel(): boolean {
91
+ if (!this.globalSocket) {
92
+ this.handleNotReady();
93
+ return false;
94
+ }
95
+
96
+ this.globalSocket.send(JSON.stringify({jsonrpc: '2.0', method: 'subscribe_transfer_activity', params: [asperaSdk.globals.appId], id: 1}));
97
+
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * This function registers clients to listen to a certain message name. Returns any to allow functions to declare proper type
103
+ *
104
+ * @param messageName - the name of messages to listen to (one message name per subscription)
105
+ * @param callback - the callback function
106
+ */
107
+ registerMessage(messageName: WebsocketTopics, callback: Function): void {
108
+ if (!this.sockets.get(messageName)) {
109
+ this.sockets.set(messageName, (data: {result: TransferResponse}) => {
110
+ callback(data.result);
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ *
117
+ * @param callback This function registers clients to a certain WebSocket event.
118
+ *
119
+ * @param callback - the callback function to call with the event name.
120
+ */
121
+ registerEvent(callback: Function): void {
122
+ this.eventListener = callback;
123
+ this.eventListener(this.isConnected ? 'RECONNECT': 'CLOSED');
124
+ }
125
+
126
+ /**
127
+ * This function starts the websocket subscription with the websocket provider
128
+ *
129
+ * @returns a promise that resolves when the websocket connection is established
130
+ */
131
+ init(): Promise<unknown> {
132
+ this.connect();
133
+
134
+ return this.initPromise.promise;
135
+ }
136
+
137
+ private connect() {
138
+ this.getWebSocketConnection(33024, 33029)
139
+ .then((webSocket) => {
140
+ this.globalSocket = webSocket;
141
+ this.globalSocket.onerror = this.handleError;
142
+ this.globalSocket.onclose = this.handleClose;
143
+ this.globalSocket.onopen = this.handleOpen;
144
+ this.globalSocket.onmessage = this.handleMessage;
145
+
146
+ this.handleOpen();
147
+ }).catch(() => {
148
+ this.reconnect();
149
+ });
150
+ }
151
+
152
+ private reconnect() {
153
+ if (this.globalSocket) {
154
+ this.globalSocket.close();
155
+ }
156
+
157
+ setTimeout(() => {
158
+ this.connect();
159
+ }, 1000);
160
+ }
161
+
162
+ private getWebSocketConnection(startPort: number, endPort: number): Promise<WebSocket> {
163
+ const webSocketUrl = getWebsocketUrl(asperaSdk.globals.asperaAppUrl);
164
+
165
+ const checkPort = (port: number): Promise<WebSocket> => {
166
+ return new Promise((resolve, reject) => {
167
+ const webSocket = new WebSocket(`${webSocketUrl}:${port}`);
168
+
169
+ webSocket.onopen = () => {
170
+ resolve(webSocket);
171
+ };
172
+
173
+ webSocket.onerror = () => {
174
+ reject(`Connection failed on port ${port}`);
175
+ };
176
+ });
177
+ };
178
+
179
+ return new Promise((resolve, reject) => {
180
+ const connectPort = (port: number) => {
181
+ if (port > endPort) {
182
+ return reject('No available WebSocket connection found');
183
+ }
184
+
185
+ checkPort(port)
186
+ .then(ws => resolve(ws))
187
+ .catch((error) => {
188
+ connectPort(port + 1);
189
+ });
190
+ };
191
+
192
+ connectPort(startPort);
193
+ });
194
+ }
195
+
196
+ private notifyEvent(event: WebsocketEvent) {
197
+ if (typeof this.eventListener === 'function') {
198
+ this.eventListener(event);
199
+ }
200
+ }
201
+
202
+ private updateRpcPort() {
203
+ if (!this.globalSocket) {
204
+ return;
205
+ }
206
+
207
+ const url = new URL(this.globalSocket.url);
208
+
209
+ asperaSdk.globals.rpcPort = Number(url.port);
210
+ }
211
+ }
212
+
213
+ export const websocketService = new WebsocketService();
214
+
215
+ export default WebsocketService;
package/src/index.html ADDED
@@ -0,0 +1,404 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <script src="aspera-sdk.js"></script>
5
+ <title>JavaScript SDK Test Server</title>
6
+ <style>
7
+ body {
8
+ padding: 30px;
9
+ }
10
+ .test-buttons {
11
+ margin: 30px 0;
12
+ }
13
+ .test-buttons button {
14
+ margin: 0 10px;
15
+ padding: 10px;
16
+ }
17
+ .flex-page {
18
+ display: flex;
19
+ }
20
+ .text-area, .transfer-monitor {
21
+ flex: 1;
22
+ padding-right: 20px;
23
+ }
24
+ .text-area * {
25
+ display: block;
26
+ }
27
+ .text-area textarea {
28
+ width: 600px;
29
+ padding: 3px;
30
+ font-size: 16px;
31
+ font-family: monospace;
32
+ height: 300px;
33
+ }
34
+ .text-area input {
35
+ font-size: 16px;
36
+ font-family: monospace;
37
+ padding: 3px;
38
+ width: 600px;
39
+ }
40
+ label {
41
+ font-weight: bold;
42
+ margin-top: 25px;
43
+ }
44
+ .transfer {
45
+ display: block;
46
+ margin-bottom: 20px;
47
+ position: relative;
48
+ }
49
+ .transfer .progress {
50
+ background-color: grey;
51
+ width: 100%;
52
+ height: 30px;
53
+ }
54
+
55
+ .transfer .bar {
56
+ background-color: green;
57
+ height: 100%;
58
+ }
59
+
60
+ .transfer .status {
61
+ position: absolute;
62
+ top: 5px;
63
+ left: 10px;
64
+ color: white;
65
+ font-weight: bold;
66
+ }
67
+
68
+ .drop-area {
69
+ height: 80px;
70
+ background-color: rgba(0,0,255,0.3);
71
+ text-align: center;
72
+ padding-top: 40px;
73
+ font-size: 18px;
74
+ margin-bottom: 20px;
75
+ font-weight: bold;
76
+ }
77
+
78
+ .loader {
79
+ border: 16px solid #f3f3f3;
80
+ border-top: 16px solid #3498db;
81
+ border-radius: 50%;
82
+ width: 120px;
83
+ height: 120px;
84
+ animation: spin 2s linear infinite;
85
+ margin: 0 auto;
86
+ }
87
+
88
+ @keyframes spin {
89
+ 0% { transform: rotate(0deg); }
90
+ 100% { transform: rotate(360deg); }
91
+ }
92
+
93
+ .error {
94
+ color: red;
95
+ margin: 15px 0;
96
+ text-align: center;
97
+ font-size: 32px;
98
+ }
99
+
100
+ .docs {
101
+ color: #343a40;
102
+ text-decoration: none;
103
+ font-family: Arial, sans-serif;
104
+ font-size: 16px;
105
+ border-bottom: 1px dashed #343a40;
106
+ transition: color 0.3s ease, border-bottom-color 0.3s ease;
107
+ }
108
+
109
+ .doc-link:hover {
110
+ color: #1d2124;
111
+ border-bottom-color: #1d2124;
112
+ }
113
+ </style>
114
+ <script>
115
+ function bytesCalc(value) {
116
+ if (value === null || value === '' || value === undefined) {
117
+ return '\u2014';
118
+ }
119
+ const base = 1024;
120
+ const units = [
121
+ 'B',
122
+ 'KB',
123
+ 'MB',
124
+ 'GB',
125
+ 'TB'
126
+ ];
127
+
128
+ let unitIndex = 0;
129
+
130
+ if (Math.abs(value) > base) {
131
+ do {
132
+ value /= base;
133
+ ++unitIndex;
134
+ } while ( Math.abs(value) >= base && unitIndex < units.length - 1 );
135
+ }
136
+ value = value.toFixed(2);
137
+ return `${value} ${units[unitIndex]}`;
138
+ }
139
+
140
+ function generateObjectText() {
141
+ const text = JSON.stringify(asperaSdk, undefined, 2);
142
+ document.querySelector('#test-output').value = text;
143
+ setTimeout(() => {
144
+ generateObjectText();
145
+ }, 1000);
146
+ }
147
+
148
+ function init(supportMultipleUsers) {
149
+ console.log('INITIALIZING...');
150
+
151
+ const appId = document.querySelector('#app-id').value;
152
+ const options = {appId, supportMultipleUsers};
153
+
154
+ asperaSdk.init(options).then(() => {
155
+ console.log('INIT SUCCESS');
156
+
157
+ asperaSdk.getAllTransfers().then(response => {
158
+ monitorTransfers({ transfers: response });
159
+ }).catch(error => {
160
+ console.log('GET ALL TRANSFERS FAIL', error);
161
+ });
162
+ }).catch(error => {
163
+ console.log('INIT FAIL', error);
164
+ })
165
+ }
166
+
167
+ function testServer() {
168
+ asperaSdk.testConnection().then(response => {
169
+ console.log('TEST SUCCESS', response);
170
+ }).catch(error => {
171
+ console.log('TEST FAIL', error);
172
+ })
173
+ }
174
+
175
+ function getInfo() {
176
+ asperaSdk.getInfo().then(response => {
177
+ console.log('GET INFO SUCCESS', response);
178
+ }).catch(error => {
179
+ console.log('GET INFO FAIL', error);
180
+ })
181
+ }
182
+
183
+ function transfer() {
184
+ const asperaSpec = {
185
+ use_absolute_destination_path: false
186
+ };
187
+
188
+ try {
189
+ const transferSpec = JSON.parse(document.querySelector('#test-input').value);
190
+ asperaSdk.startTransfer(transferSpec, asperaSpec).then(response => {
191
+ console.log('TRANSFER SUCCESS', response);
192
+ }).catch(error => {
193
+ console.log('TRANSFER FAIL', error);
194
+ })
195
+ } catch (error) {
196
+ console.log('Unable to parse transferSpec', error);
197
+ }
198
+ }
199
+
200
+ function remove(id) {
201
+ asperaSdk.removeTransfer(id).then(response => {
202
+ console.log('REMOVE SUCCESS', response);
203
+ }).catch(error => {
204
+ console.log('REMOVE FAIL', error);
205
+ })
206
+ }
207
+
208
+ function stopTransfer(id) {
209
+ asperaSdk.stopTransfer(id).then(response => {
210
+ console.log('STOP SUCCESS', response);
211
+ }).catch(error => {
212
+ console.log('STOP FAIL', error);
213
+ })
214
+ }
215
+
216
+ function resumeTransfer(id) {
217
+ try {
218
+ const transferSpec = JSON.parse(document.querySelector('#test-input').value);
219
+ asperaSdk.resumeTransfer(id, transferSpec).then(response => {
220
+ console.log('RESUME SUCCESS', response);
221
+ }).catch(error => {
222
+ console.log('RESUME FAIL', error);
223
+ })
224
+ } catch (error) {
225
+ console.log('Unable to parse transferSpec for resume', error);
226
+ }
227
+ }
228
+
229
+ function getAllTransfers() {
230
+ asperaSdk.getAllTransfers().then(response => {
231
+ console.log('GET ALL TRANSFERS SUCCESS', response);
232
+ }).catch(error => {
233
+ console.log('GET ALL TRANSFERS FAIL', error);
234
+ });
235
+ }
236
+
237
+ function getTransfer(id) {
238
+ asperaSdk.getTransfer(id).then(response => {
239
+ console.log('GET TRANSFER SUCCESS', response);
240
+ }).catch(error => {
241
+ console.log('GET TRANSFER FAIL', error);
242
+ });
243
+ }
244
+
245
+ function showDirectory(id) {
246
+ asperaSdk.showDirectory(id).then(response => {
247
+ console.log('SHOW DIRECTORY SUCCESS', response);
248
+ }).catch(error => {
249
+ console.log('SHOW DIRECTORY FAIL', error);
250
+ });
251
+ }
252
+
253
+ function showSelectFileDialog() {
254
+ asperaSdk.showSelectFileDialog().then(response => {
255
+ console.log('SHOW FILE DIALOG SUCCESS', response);
256
+ }).catch(error => {
257
+ console.log('SHOW FILE DIALOG FAIL', error);
258
+ });
259
+ }
260
+
261
+ function showSelectFolderDialog() {
262
+ asperaSdk.showSelectFolderDialog().then(response => {
263
+ console.log('SHOW FOLDER DIALOG SUCCESS', response);
264
+ }).catch(error => {
265
+ console.log('SHOW FOLDER DIALOG FAIL', error);
266
+ });
267
+ }
268
+
269
+ function openPreferences() {
270
+ asperaSdk.showPreferences().then(response => {
271
+ console.log('SHOW PREFERENCES SUCCESS', response);
272
+ }).catch(error => {
273
+ console.log('SHOW PREFERENCES FAIL', error);
274
+ });
275
+ }
276
+
277
+ function modifyTransfer(id) {
278
+ const options = {
279
+ target_rate_kbps: 10,
280
+ };
281
+
282
+ asperaSdk.modifyTransfer(id, options).then(response => {
283
+ console.log('MODIFY TRANSFER SUCCESS', response);
284
+ }).catch(error => {
285
+ console.log('MODIFY TRANSFER FAIL', error);
286
+ });
287
+ }
288
+
289
+ function testDrop() {
290
+ const callback = files => {
291
+ console.log('DROP ZONE CALLBACK', files);
292
+ }
293
+
294
+ asperaSdk.createDropzone(callback, '#drop-area');
295
+ }
296
+
297
+ function getInstallerInfo() {
298
+ asperaSdk.getInstallerInfo().then(response => {
299
+ console.log('INSTALLER INFO SUCCESS', response);
300
+ }).catch(error => {
301
+ console.log('INSTALLER INFO FAIL', error);
302
+ })
303
+ }
304
+
305
+ function monitorRemovedTransfers(transfer) {
306
+ console.log('TRANSFER WAS REMOVED', transfer);
307
+ }
308
+
309
+ function monitorTransfers(transfers) {
310
+ const transferDom = document.querySelector('#transfer');
311
+ while (transferDom.firstChild) {
312
+ transferDom.removeChild(transferDom.firstChild);
313
+ }
314
+ if (!transfers.transfers.length) {
315
+ const element = document.createElement('div');
316
+ element.setAttribute('class', 'transfer');
317
+ element.innerHTML = `
318
+ <div class="name">No transfers</div>
319
+ `;
320
+ transferDom.appendChild(element);
321
+ return;
322
+ }
323
+ transfers.transfers.forEach(transfer => {
324
+ const element = document.createElement('div');
325
+ element.setAttribute('class', 'transfer-item');
326
+ element.innerHTML = `
327
+ <div class="progress">
328
+ <div class="bar" style="width: ${(transfer.percentage * 100).toFixed(2)}%;"></div>
329
+ </div>
330
+ <div class="status">${transfer.status} - ${bytesCalc(transfer.bytes_written)} / ${bytesCalc(transfer.bytes_expected)}</div>
331
+ <button onclick=remove("${transfer.uuid}")>Remove</button>
332
+ <button onclick=stopTransfer("${transfer.uuid}")>Stop</button>
333
+ <button onclick=resumeTransfer("${transfer.uuid}")>Resume</button>
334
+ <button onclick=getTransfer("${transfer.uuid}")>Info</button>
335
+ <button onclick=showDirectory("${transfer.uuid}")>Show</button>
336
+ <button onclick=modifyTransfer("${transfer.uuid}")>Modify</button>
337
+ `;
338
+ transferDom.appendChild(element);
339
+ });
340
+ }
341
+
342
+ function launch() {
343
+ asperaSdk.launch();
344
+ }
345
+
346
+ function initDragDrop() {
347
+ asperaSdk.initDragDrop();
348
+ }
349
+
350
+ setTimeout(() => {
351
+ generateObjectText();
352
+ }, 3000);
353
+
354
+ asperaSdk.registerActivityCallback(monitorTransfers);
355
+ asperaSdk.registerRemovedCallback(monitorRemovedTransfers);
356
+
357
+ asperaSdk.registerStatusCallback((status) => {
358
+ console.log('Websocket status changed to:', status);
359
+ });
360
+
361
+ if (asperaSdk.isSafari()) {
362
+ asperaSdk.registerSafariExtensionStatusCallback((status) => {
363
+ console.log('Safari extension status changed to:', status);
364
+ });
365
+ }
366
+ </script>
367
+ </head>
368
+ <body>
369
+ <h1>Aspera SDK Test Server</h1>
370
+ <a href="/aspera-sdk-js/docs/index.html" class="docs" target="_blank">Documentation</a>
371
+ <div class="test-buttons">
372
+ <button onclick="init(false)" type="button">Init SDK</button>
373
+ <button onclick="init(true)" type="button">Init SDK with session</button>
374
+ <button onclick="testServer()" type="button">Test desktop</button>
375
+ <button onclick="getInfo()" type="button">Get Info</button>
376
+ <button onclick="transfer()" type="button">Start transfer</button>
377
+ <button onclick="getAllTransfers()" type="button">Get transfers</button>
378
+ <button onclick="showSelectFileDialog()" type="button">Select File</button>
379
+ <button onclick="showSelectFolderDialog()" type="button">Select Folder</button>
380
+ <button onclick="openPreferences()" type="button">Open Preferences</button>
381
+ <button onclick="launch()" type="button">Launch</button>
382
+ <button onclick="testDrop()" type="button">Set dropzone</button>
383
+ <button onclick="getInstallerInfo()" type="button">Installers</button>
384
+ <button onclick="initDragDrop()" type="button">Init drag-drop</button>
385
+ </div>
386
+ <div class="flex-page">
387
+ <div class="text-area">
388
+ <label for="app-id">App ID</label>
389
+ <input type="text" id="app-id" value="test1"></input>
390
+ <label for="test-input">transferSpec for download/upload</label>
391
+ <textarea id="test-input"></textarea>
392
+ <label for="test-output">Current values in asperaSdk</label>
393
+ <textarea id="test-output" readonly></textarea>
394
+ </div>
395
+ <div id="transfer-monitor" class="transfer-monitor">
396
+ <br />
397
+ <hl />
398
+ <p>Drop test</p>
399
+ <div class="drop-area" id="drop-area">Drop area</div>
400
+ <div id="transfer" class="transfer"><div class="name">No transfers</div></div>
401
+ </div>
402
+ </div>
403
+ </body>
404
+ </html>