1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | "use strict";
|
29 |
|
30 | var supportDir;
|
31 |
|
32 | var semver = require("semver"),
|
33 | path = require("path"),
|
34 | http = require("http"),
|
35 | request = require("request"),
|
36 | os = require("os"),
|
37 | fs = require("fs-extra"),
|
38 | temp = require("temp"),
|
39 | validate = require("./package-validator").validate;
|
40 |
|
41 |
|
42 | temp.track();
|
43 |
|
44 | var Errors = {
|
45 | API_NOT_COMPATIBLE: "API_NOT_COMPATIBLE",
|
46 | MISSING_REQUIRED_OPTIONS: "MISSING_REQUIRED_OPTIONS",
|
47 | DOWNLOAD_ID_IN_USE: "DOWNLOAD_ID_IN_USE",
|
48 | BAD_HTTP_STATUS: "BAD_HTTP_STATUS",
|
49 | NO_SERVER_RESPONSE: "NO_SERVER_RESPONSE",
|
50 | CANNOT_WRITE_TEMP: "CANNOT_WRITE_TEMP",
|
51 | CANCELED: "CANCELED"
|
52 | };
|
53 |
|
54 | var Statuses = {
|
55 | FAILED: "FAILED",
|
56 | INSTALLED: "INSTALLED",
|
57 | ALREADY_INSTALLED: "ALREADY_INSTALLED",
|
58 | SAME_VERSION: "SAME_VERSION",
|
59 | OLDER_VERSION: "OLDER_VERSION",
|
60 | NEEDS_UPDATE: "NEEDS_UPDATE",
|
61 | DISABLED: "DISABLED"
|
62 | };
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | var pendingDownloads = {};
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | function _removeFailedInstallation(installDirectory) {
|
80 | fs.remove(installDirectory, function (err) {
|
81 | if (err) {
|
82 | console.error("Error while removing directory after failed installation", installDirectory, err);
|
83 | }
|
84 | });
|
85 | }
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | function _performInstall(packagePath, installDirectory, validationResult, callback) {
|
96 | validationResult.installedTo = installDirectory;
|
97 |
|
98 | var callbackCalled = false;
|
99 |
|
100 | fs.mkdirs(installDirectory, function (err) {
|
101 | if (err) {
|
102 | callback(err);
|
103 | return;
|
104 | }
|
105 | var sourceDir = path.join(validationResult.extractDir, validationResult.commonPrefix);
|
106 |
|
107 | fs.copy(sourceDir, installDirectory, function (err) {
|
108 | if (err) {
|
109 | _removeFailedInstallation(installDirectory);
|
110 | callback(err, null);
|
111 | } else {
|
112 |
|
113 |
|
114 | if (!validationResult.installationStatus) {
|
115 | validationResult.installationStatus = Statuses.INSTALLED;
|
116 | }
|
117 | callback(null, validationResult);
|
118 | }
|
119 | });
|
120 | });
|
121 | }
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 | function _removeAndInstall(packagePath, installDirectory, validationResult, callback) {
|
132 |
|
133 |
|
134 | fs.remove(installDirectory, function (err) {
|
135 | if (err) {
|
136 | callback(err);
|
137 | return;
|
138 | }
|
139 | _performInstall(packagePath, installDirectory, validationResult, callback);
|
140 | });
|
141 | }
|
142 |
|
143 | function _checkExistingInstallation(validationResult, installDirectory, systemInstallDirectory, callback) {
|
144 |
|
145 |
|
146 |
|
147 | if (!validationResult.metadata) {
|
148 | validationResult.installationStatus = Statuses.ALREADY_INSTALLED;
|
149 | callback(null, validationResult);
|
150 | return;
|
151 | }
|
152 |
|
153 | fs.readJson(path.join(installDirectory, "package.json"), function (err, packageObj) {
|
154 |
|
155 |
|
156 | if (err) {
|
157 | validationResult.installationStatus = Statuses.NEEDS_UPDATE;
|
158 | } else {
|
159 |
|
160 | if (semver.lt(packageObj.version, validationResult.metadata.version)) {
|
161 | validationResult.installationStatus = Statuses.NEEDS_UPDATE;
|
162 | } else if (semver.gt(packageObj.version, validationResult.metadata.version)) {
|
163 |
|
164 |
|
165 | validationResult.installationStatus = Statuses.OLDER_VERSION;
|
166 | validationResult.installedVersion = packageObj.version;
|
167 | } else {
|
168 |
|
169 |
|
170 | validationResult.installationStatus = Statuses.SAME_VERSION;
|
171 | }
|
172 | }
|
173 | callback(null, validationResult);
|
174 | });
|
175 | }
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 | function legacyPackageCheck(legacyDirectory) {
|
186 | return fs.existsSync(legacyDirectory) && !fs.existsSync(path.join(legacyDirectory, "package.json"));
|
187 | }
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | function _cmdInstall(packagePath, destinationDirectory, options, callback, _doUpdate) {
|
217 | if (!options || !options.disabledDirectory || !options.apiVersion || !options.systemExtensionDirectory) {
|
218 | callback(new Error(Errors.MISSING_REQUIRED_OPTIONS), null);
|
219 | return;
|
220 | }
|
221 |
|
222 | var validateCallback = function (err, validationResult) {
|
223 | validationResult.localPath = packagePath;
|
224 |
|
225 | if (destinationDirectory.indexOf("/support/") === 0) {
|
226 | destinationDirectory = path.join(supportDir, destinationDirectory.substr(8));
|
227 | }
|
228 |
|
229 |
|
230 |
|
231 | function deleteTempAndCallback(err) {
|
232 | if (validationResult.extractDir) {
|
233 | fs.remove(validationResult.extractDir);
|
234 | delete validationResult.extractDir;
|
235 | }
|
236 | callback(err, validationResult);
|
237 | }
|
238 |
|
239 |
|
240 | if (err || validationResult.errors.length > 0) {
|
241 | validationResult.installationStatus = Statuses.FAILED;
|
242 | deleteTempAndCallback(err, validationResult);
|
243 | return;
|
244 | }
|
245 |
|
246 |
|
247 |
|
248 | var extensionName, guessedName;
|
249 | if (options.nameHint) {
|
250 | guessedName = path.basename(options.nameHint, ".zip");
|
251 | } else {
|
252 | guessedName = path.basename(packagePath, ".zip");
|
253 | }
|
254 | if (validationResult.metadata) {
|
255 | extensionName = validationResult.metadata.name;
|
256 | } else {
|
257 | extensionName = guessedName;
|
258 | }
|
259 |
|
260 | validationResult.name = extensionName;
|
261 | var installDirectory = path.join(destinationDirectory, extensionName),
|
262 | legacyDirectory = path.join(destinationDirectory, guessedName),
|
263 | systemInstallDirectory = path.join(options.systemExtensionDirectory, extensionName);
|
264 |
|
265 | if (validationResult.metadata && validationResult.metadata.engines &&
|
266 | validationResult.metadata.engines.brackets) {
|
267 | var compatible = semver.satisfies(options.apiVersion,
|
268 | validationResult.metadata.engines.brackets);
|
269 | if (!compatible) {
|
270 | installDirectory = path.join(options.disabledDirectory, extensionName);
|
271 | validationResult.installationStatus = Statuses.DISABLED;
|
272 | validationResult.disabledReason = Errors.API_NOT_COMPATIBLE;
|
273 | _removeAndInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback);
|
274 | return;
|
275 | }
|
276 | }
|
277 |
|
278 |
|
279 |
|
280 | var hasLegacyPackage = validationResult.metadata && legacyPackageCheck(legacyDirectory);
|
281 |
|
282 |
|
283 |
|
284 | if (hasLegacyPackage || fs.existsSync(installDirectory) || fs.existsSync(systemInstallDirectory)) {
|
285 | if (_doUpdate) {
|
286 | if (hasLegacyPackage) {
|
287 |
|
288 |
|
289 |
|
290 |
|
291 | fs.remove(legacyDirectory, function (err) {
|
292 | if (err) {
|
293 | deleteTempAndCallback(err, validationResult);
|
294 | return;
|
295 | }
|
296 | _removeAndInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback);
|
297 | });
|
298 | } else {
|
299 | _removeAndInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback);
|
300 | }
|
301 | } else if (hasLegacyPackage) {
|
302 | validationResult.installationStatus = Statuses.NEEDS_UPDATE;
|
303 | validationResult.name = guessedName;
|
304 | deleteTempAndCallback(null, validationResult);
|
305 | } else {
|
306 | _checkExistingInstallation(validationResult, installDirectory, systemInstallDirectory, deleteTempAndCallback);
|
307 | }
|
308 | } else {
|
309 |
|
310 | validationResult.disabledReason = null;
|
311 | _performInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback);
|
312 | }
|
313 | };
|
314 |
|
315 | validate(packagePath, {}, validateCallback);
|
316 | }
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 |
|
347 | function _cmdUpdate(packagePath, destinationDirectory, options, callback) {
|
348 | _cmdInstall(packagePath, destinationDirectory, options, callback, true);
|
349 | }
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 | function _endDownload(downloadId, error) {
|
359 | var downloadInfo = pendingDownloads[downloadId];
|
360 | delete pendingDownloads[downloadId];
|
361 |
|
362 | if (error) {
|
363 |
|
364 |
|
365 | downloadInfo.request.abort();
|
366 |
|
367 |
|
368 |
|
369 | if (downloadInfo.outStream) {
|
370 | downloadInfo.outStream.end(function () {
|
371 | fs.unlink(downloadInfo.localPath);
|
372 | });
|
373 | }
|
374 |
|
375 | downloadInfo.callback(error, null);
|
376 |
|
377 | } else {
|
378 |
|
379 | downloadInfo.outStream.end(function () {
|
380 | downloadInfo.callback(null, downloadInfo.localPath);
|
381 | });
|
382 | }
|
383 | }
|
384 |
|
385 |
|
386 |
|
387 |
|
388 | function _cmdDownloadFile(downloadId, url, proxy, callback) {
|
389 |
|
390 | if (typeof proxy === "function") {
|
391 | callback = proxy;
|
392 | proxy = undefined;
|
393 | }
|
394 |
|
395 | if (pendingDownloads[downloadId]) {
|
396 | callback(Errors.DOWNLOAD_ID_IN_USE, null);
|
397 | return;
|
398 | }
|
399 |
|
400 | var https = require("https");
|
401 |
|
402 | var req = https.get(url, function(res) {
|
403 | if (res.statusCode !== 200) {
|
404 | _endDownload(downloadId, [Errors.BAD_HTTP_STATUS, res.statusCode]);
|
405 | return;
|
406 | }
|
407 |
|
408 | var stream = temp.createWriteStream("brackets");
|
409 | if (!stream) {
|
410 | _endDownload(downloadId, Errors.CANNOT_WRITE_TEMP);
|
411 | return;
|
412 | }
|
413 | pendingDownloads[downloadId].localPath = stream.path;
|
414 | pendingDownloads[downloadId].outStream = stream;
|
415 |
|
416 | res.on("data", function(d) {
|
417 | stream.write(d);
|
418 | });
|
419 |
|
420 | res.on("end", function () {
|
421 | _endDownload(downloadId);
|
422 | });
|
423 |
|
424 | }).on("error", function(e) {
|
425 | console.error(e);
|
426 | _endDownload(downloadId, e.message);
|
427 | });
|
428 |
|
429 | pendingDownloads[downloadId] = { request: req, callback: callback };
|
430 | }
|
431 |
|
432 |
|
433 |
|
434 |
|
435 | function _cmdAbortDownload(downloadId) {
|
436 | if (!pendingDownloads[downloadId]) {
|
437 |
|
438 | return false;
|
439 | } else {
|
440 | _endDownload(downloadId, Errors.CANCELED);
|
441 | return true;
|
442 | }
|
443 | }
|
444 |
|
445 |
|
446 |
|
447 |
|
448 | function _cmdRemove(extensionDir, callback) {
|
449 | if (extensionDir.indexOf("/support/") === 0) {
|
450 | extensionDir = path.join(supportDir, extensionDir.substr(8));
|
451 | }
|
452 |
|
453 | fs.remove(extensionDir, function (err) {
|
454 | if (err) {
|
455 | callback(err);
|
456 | } else {
|
457 | callback(null);
|
458 | }
|
459 | });
|
460 | }
|
461 |
|
462 |
|
463 |
|
464 |
|
465 |
|
466 | function init(domainManager) {
|
467 | supportDir = domainManager.supportDir;
|
468 | if (!domainManager.hasDomain("extensionManager")) {
|
469 | domainManager.registerDomain("extensionManager", {major: 0, minor: 1});
|
470 | }
|
471 | domainManager.registerCommand(
|
472 | "extensionManager",
|
473 | "validate",
|
474 | validate,
|
475 | true,
|
476 | "Verifies that the contents of the given ZIP file are a valid Brackets extension package",
|
477 | [{
|
478 | name: "path",
|
479 | type: "string",
|
480 | description: "absolute filesystem path of the extension package"
|
481 | }, {
|
482 | name: "options",
|
483 | type: "{requirePackageJSON: ?boolean}",
|
484 | description: "options to control the behavior of the validator"
|
485 | }],
|
486 | [{
|
487 | name: "errors",
|
488 | type: "string|Array.<string>",
|
489 | description: "download error, if any; first string is error code (one of Errors.*); subsequent strings are additional info"
|
490 | }, {
|
491 | name: "metadata",
|
492 | type: "{name: string, version: string}",
|
493 | description: "all package.json metadata (null if there's no package.json)"
|
494 | }]
|
495 | );
|
496 | domainManager.registerCommand(
|
497 | "extensionManager",
|
498 | "install",
|
499 | _cmdInstall,
|
500 | true,
|
501 | "Installs the given Brackets extension if it is valid (runs validation command automatically)",
|
502 | [{
|
503 | name: "path",
|
504 | type: "string",
|
505 | description: "absolute filesystem path of the extension package"
|
506 | }, {
|
507 | name: "destinationDirectory",
|
508 | type: "string",
|
509 | description: "absolute filesystem path where this extension should be installed"
|
510 | }, {
|
511 | name: "options",
|
512 | type: "{disabledDirectory: !string, apiVersion: !string, nameHint: ?string, systemExtensionDirectory: !string}",
|
513 | description: "installation options: disabledDirectory should be set so that extensions can be installed disabled."
|
514 | }],
|
515 | [{
|
516 | name: "errors",
|
517 | type: "string|Array.<string>",
|
518 | description: "download error, if any; first string is error code (one of Errors.*); subsequent strings are additional info"
|
519 | }, {
|
520 | name: "metadata",
|
521 | type: "{name: string, version: string}",
|
522 | description: "all package.json metadata (null if there's no package.json)"
|
523 | }, {
|
524 | name: "disabledReason",
|
525 | type: "string",
|
526 | description: "reason this extension was installed disabled (one of Errors.*), none if it was enabled"
|
527 | }, {
|
528 | name: "installationStatus",
|
529 | type: "string",
|
530 | description: "Current status of the installation (an extension can be valid but not installed because it's an update"
|
531 | }, {
|
532 | name: "installedTo",
|
533 | type: "string",
|
534 | description: "absolute path where the extension was installed to"
|
535 | }, {
|
536 | name: "commonPrefix",
|
537 | type: "string",
|
538 | description: "top level directory in the package zip which contains all of the files"
|
539 | }]
|
540 | );
|
541 | domainManager.registerCommand(
|
542 | "extensionManager",
|
543 | "update",
|
544 | _cmdUpdate,
|
545 | true,
|
546 | "Updates the given Brackets extension (for which install was generally previously attemped). Brackets must be quit after this.",
|
547 | [{
|
548 | name: "path",
|
549 | type: "string",
|
550 | description: "absolute filesystem path of the extension package"
|
551 | }, {
|
552 | name: "destinationDirectory",
|
553 | type: "string",
|
554 | description: "absolute filesystem path where this extension should be installed"
|
555 | }, {
|
556 | name: "options",
|
557 | type: "{disabledDirectory: !string, apiVersion: !string, nameHint: ?string, systemExtensionDirectory: !string}",
|
558 | description: "installation options: disabledDirectory should be set so that extensions can be installed disabled."
|
559 | }],
|
560 | [{
|
561 | name: "errors",
|
562 | type: "string|Array.<string>",
|
563 | description: "download error, if any; first string is error code (one of Errors.*); subsequent strings are additional info"
|
564 | }, {
|
565 | name: "metadata",
|
566 | type: "{name: string, version: string}",
|
567 | description: "all package.json metadata (null if there's no package.json)"
|
568 | }, {
|
569 | name: "disabledReason",
|
570 | type: "string",
|
571 | description: "reason this extension was installed disabled (one of Errors.*), none if it was enabled"
|
572 | }, {
|
573 | name: "installationStatus",
|
574 | type: "string",
|
575 | description: "Current status of the installation (an extension can be valid but not installed because it's an update"
|
576 | }, {
|
577 | name: "installedTo",
|
578 | type: "string",
|
579 | description: "absolute path where the extension was installed to"
|
580 | }, {
|
581 | name: "commonPrefix",
|
582 | type: "string",
|
583 | description: "top level directory in the package zip which contains all of the files"
|
584 | }]
|
585 | );
|
586 | domainManager.registerCommand(
|
587 | "extensionManager",
|
588 | "remove",
|
589 | _cmdRemove,
|
590 | true,
|
591 | "Removes the Brackets extension at the given path.",
|
592 | [{
|
593 | name: "path",
|
594 | type: "string",
|
595 | description: "absolute filesystem path of the installed extension folder"
|
596 | }],
|
597 | {}
|
598 | );
|
599 | domainManager.registerCommand(
|
600 | "extensionManager",
|
601 | "downloadFile",
|
602 | _cmdDownloadFile,
|
603 | true,
|
604 | "Downloads the file at the given URL, saving it to a temp location. Callback receives path to the downloaded file.",
|
605 | [{
|
606 | name: "downloadId",
|
607 | type: "string",
|
608 | description: "Unique identifier for this download 'session'"
|
609 | }, {
|
610 | name: "url",
|
611 | type: "string",
|
612 | description: "URL to download from"
|
613 | }, {
|
614 | name: "proxy",
|
615 | type: "string",
|
616 | description: "optional proxy URL"
|
617 | }],
|
618 | {
|
619 | type: "string",
|
620 | description: "Local path to the downloaded file"
|
621 | }
|
622 | );
|
623 | domainManager.registerCommand(
|
624 | "extensionManager",
|
625 | "abortDownload",
|
626 | _cmdAbortDownload,
|
627 | false,
|
628 | "Aborts any pending download with the given id. Ignored if no download pending (may be already complete).",
|
629 | [{
|
630 | name: "downloadId",
|
631 | type: "string",
|
632 | description: "Unique identifier for this download 'session', previously pased to downloadFile"
|
633 | }],
|
634 | {
|
635 | type: "boolean",
|
636 | description: "True if the download was pending and able to be canceled; false otherwise"
|
637 | }
|
638 | );
|
639 | }
|
640 |
|
641 |
|
642 | exports._cmdValidate = validate;
|
643 | exports._cmdInstall = _cmdInstall;
|
644 | exports._cmdRemove = _cmdRemove;
|
645 | exports._cmdUpdate = _cmdUpdate;
|
646 |
|
647 |
|
648 | exports.init = init;
|