toolkit/webapps/LinuxNativeApp.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:1dcc372afff1
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 /**
6 * Constructor for the Linux native app shell
7 *
8 * @param aApp {Object} the app object provided to the install function
9 * @param aManifest {Object} the manifest data provided by the web app
10 * @param aCategories {Array} array of app categories
11 * @param aRegistryDir {String} (optional) path to the registry
12 */
13 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
14 CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
15
16 this.iconFile = "icon.png";
17 this.webapprt = "webapprt-stub";
18 this.configJson = "webapp.json";
19 this.webappINI = "webapp.ini";
20 this.zipFile = "application.zip";
21
22 this.backupFiles = [ this.iconFile, this.configJson, this.webappINI ];
23 if (this.isPackaged) {
24 this.backupFiles.push(this.zipFile);
25 }
26
27 let xdg_data_home = Cc["@mozilla.org/process/environment;1"].
28 getService(Ci.nsIEnvironment).
29 get("XDG_DATA_HOME");
30 if (!xdg_data_home) {
31 xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share");
32 }
33
34 // The desktop file name is: "owa-" + sanitized app name +
35 // "-" + manifest url hash.
36 this.desktopINI = OS.Path.join(xdg_data_home, "applications",
37 "owa-" + this.uniqueName + ".desktop");
38 }
39
40 NativeApp.prototype = {
41 __proto__: CommonNativeApp.prototype,
42
43 /**
44 * Creates a native installation of the web app in the OS
45 *
46 * @param aManifest {Object} the manifest data provided by the web app
47 * @param aZipPath {String} path to the zip file for packaged apps (undefined
48 * for hosted apps)
49 */
50 install: Task.async(function*(aManifest, aZipPath) {
51 if (this._dryRun) {
52 return;
53 }
54
55 // If the application is already installed, this is a reinstallation.
56 if (WebappOSUtils.getInstallPath(this.app)) {
57 return yield this.prepareUpdate(aManifest, aZipPath);
58 }
59
60 this._setData(aManifest);
61
62 // The installation directory name is: sanitized app name +
63 // "-" + manifest url hash.
64 let installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName);
65
66 let dir = getFile(TMP_DIR, this.uniqueName);
67 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
68 let tmpDir = dir.path;
69
70 // Create the installation in a temporary directory.
71 try {
72 this._copyPrebuiltFiles(tmpDir);
73 yield this._createConfigFiles(tmpDir);
74
75 if (aZipPath) {
76 yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
77 }
78
79 yield this._getIcon(tmpDir);
80 } catch (ex) {
81 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
82 throw ex;
83 }
84
85 // Apply the installation.
86 this._removeInstallation(true, installDir);
87
88 try {
89 yield this._applyTempInstallation(tmpDir, installDir);
90 } catch (ex) {
91 this._removeInstallation(false, installDir);
92 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
93 throw ex;
94 }
95 }),
96
97 /**
98 * Creates an update in a temporary directory to be applied later.
99 *
100 * @param aManifest {Object} the manifest data provided by the web app
101 * @param aZipPath {String} path to the zip file for packaged apps (undefined
102 * for hosted apps)
103 */
104 prepareUpdate: Task.async(function*(aManifest, aZipPath) {
105 if (this._dryRun) {
106 return;
107 }
108
109 this._setData(aManifest);
110
111 let installDir = WebappOSUtils.getInstallPath(this.app);
112 if (!installDir) {
113 throw ERR_NOT_INSTALLED;
114 }
115
116 let baseName = OS.Path.basename(installDir)
117 let oldUniqueName = baseName.substring(1, baseName.length);
118 if (this.uniqueName != oldUniqueName) {
119 // Bug 919799: If the app is still in the registry, migrate its data to
120 // the new format.
121 throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
122 }
123
124 let updateDir = OS.Path.join(installDir, "update");
125 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
126 yield OS.File.makeDir(updateDir);
127
128 try {
129 yield this._createConfigFiles(updateDir);
130
131 if (aZipPath) {
132 yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
133 }
134
135 yield this._getIcon(updateDir);
136 } catch (ex) {
137 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
138 throw ex;
139 }
140 }),
141
142 /**
143 * Applies an update.
144 */
145 applyUpdate: Task.async(function*() {
146 if (this._dryRun) {
147 return;
148 }
149
150 let installDir = WebappOSUtils.getInstallPath(this.app);
151 let updateDir = OS.Path.join(installDir, "update");
152
153 let backupDir = yield this._backupInstallation(installDir);
154
155 try {
156 yield this._applyTempInstallation(updateDir, installDir);
157 } catch (ex) {
158 yield this._restoreInstallation(backupDir, installDir);
159 throw ex;
160 } finally {
161 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
162 yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
163 }
164 }),
165
166 _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
167 yield moveDirectory(aTmpDir, aInstallDir);
168
169 this._createSystemFiles(aInstallDir);
170 }),
171
172 _removeInstallation: function(keepProfile, aInstallDir) {
173 let filesToRemove = [this.desktopINI];
174
175 if (keepProfile) {
176 for (let filePath of this.backupFiles) {
177 filesToRemove.push(OS.Path.join(aInstallDir, filePath));
178 }
179
180 filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt));
181 } else {
182 filesToRemove.push(aInstallDir);
183 }
184
185 removeFiles(filesToRemove);
186 },
187
188 _backupInstallation: Task.async(function*(aInstallDir) {
189 let backupDir = OS.Path.join(aInstallDir, "backup");
190 yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
191 yield OS.File.makeDir(backupDir);
192
193 for (let filePath of this.backupFiles) {
194 yield OS.File.move(OS.Path.join(aInstallDir, filePath),
195 OS.Path.join(backupDir, filePath));
196 }
197
198 return backupDir;
199 }),
200
201 _restoreInstallation: function(aBackupDir, aInstallDir) {
202 return moveDirectory(aBackupDir, aInstallDir);
203 },
204
205 _copyPrebuiltFiles: function(aDir) {
206 let destDir = getFile(aDir);
207 let stub = getFile(this.runtimeFolder, this.webapprt);
208 stub.copyTo(destDir, null);
209 },
210
211 /**
212 * Translate marketplace categories to freedesktop.org categories.
213 *
214 * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry
215 *
216 * @return an array of categories
217 */
218 _translateCategories: function() {
219 let translations = {
220 "books": "Education;Literature",
221 "business": "Finance",
222 "education": "Education",
223 "entertainment": "Amusement",
224 "sports": "Sports",
225 "games": "Game",
226 "health-fitness": "MedicalSoftware",
227 "lifestyle": "Amusement",
228 "music": "Audio;Music",
229 "news-weather": "News",
230 "photo-video": "Video;AudioVideo;Photography",
231 "productivity": "Office",
232 "shopping": "Amusement",
233 "social": "Chat",
234 "travel": "Amusement",
235 "reference": "Science;Education;Documentation",
236 "maps-navigation": "Maps",
237 "utilities": "Utility"
238 };
239
240 // The trailing semicolon is needed as written in the freedesktop specification
241 let categories = "";
242 for (let category of this.categories) {
243 let catLower = category.toLowerCase();
244 if (catLower in translations) {
245 categories += translations[catLower] + ";";
246 }
247 }
248
249 return categories;
250 },
251
252 _createConfigFiles: function(aDir) {
253 // ${InstallDir}/webapp.json
254 yield writeToFile(OS.Path.join(aDir, this.configJson),
255 JSON.stringify(this.webappJson));
256
257 let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
258
259 // ${InstallDir}/webapp.ini
260 let webappINIfile = getFile(aDir, this.webappINI);
261
262 let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
263 getService(Ci.nsIINIParserFactory).
264 createINIParser(webappINIfile).
265 QueryInterface(Ci.nsIINIParserWriter);
266 writer.setString("Webapp", "Name", this.appName);
267 writer.setString("Webapp", "Profile", this.uniqueName);
268 writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1));
269 writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
270 writer.writeFile();
271 },
272
273 _createSystemFiles: function(aInstallDir) {
274 let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
275
276 let webapprtPath = OS.Path.join(aInstallDir, this.webapprt);
277
278 // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop
279 let desktopINIfile = getFile(this.desktopINI);
280 if (desktopINIfile.parent && !desktopINIfile.parent.exists()) {
281 desktopINIfile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
282 }
283
284 let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
285 getService(Ci.nsIINIParserFactory).
286 createINIParser(desktopINIfile).
287 QueryInterface(Ci.nsIINIParserWriter);
288 writer.setString("Desktop Entry", "Name", this.appName);
289 writer.setString("Desktop Entry", "Comment", this.shortDescription);
290 writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"');
291 writer.setString("Desktop Entry", "Icon", OS.Path.join(aInstallDir,
292 this.iconFile));
293 writer.setString("Desktop Entry", "Type", "Application");
294 writer.setString("Desktop Entry", "Terminal", "false");
295
296 let categories = this._translateCategories();
297 if (categories)
298 writer.setString("Desktop Entry", "Categories", categories);
299
300 writer.setString("Desktop Entry", "Actions", "Uninstall;");
301 writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label"));
302 writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove");
303
304 writer.writeFile();
305
306 desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR;
307 },
308
309 /**
310 * Process the icon from the imageStream as retrieved from
311 * the URL by getIconForApp().
312 *
313 * @param aMimeType ahe icon mimetype
314 * @param aImageStream the stream for the image data
315 * @param aDir the directory where the icon should be stored
316 */
317 _processIcon: function(aMimeType, aImageStream, aDir) {
318 let deferred = Promise.defer();
319
320 let imgTools = Cc["@mozilla.org/image/tools;1"].
321 createInstance(Ci.imgITools);
322
323 let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
324 let iconStream = imgTools.encodeImage(imgContainer, "image/png");
325
326 let iconFile = getFile(aDir, this.iconFile);
327 let outputStream = FileUtils.openSafeFileOutputStream(iconFile);
328 NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
329 if (Components.isSuccessCode(aResult)) {
330 deferred.resolve();
331 } else {
332 deferred.reject("Failure copying icon: " + aResult);
333 }
334 });
335
336 return deferred.promise;
337 }
338 }

mercurial