diff --git a/lib/shared/theme.js b/lib/shared/theme.js index ae14422e..1de50550 100644 --- a/lib/shared/theme.js +++ b/lib/shared/theme.js @@ -196,17 +196,50 @@ export class ThemeManagerBase extends GObject.Object { if (this._needUpdate()) { let originalCss = this.configMgr.defaultStylesheetFile; let configCss = this.configMgr.stylesheetFile; - let copyConfigCss = Gio.File.new_for_path(this.configMgr.stylesheetFileName + ".bak"); - let backupFine = configCss.copy(copyConfigCss, Gio.FileCopyFlags.OVERWRITE, null, null); - let copyFine = originalCss.copy(configCss, Gio.FileCopyFlags.OVERWRITE, null, null); - if (backupFine && copyFine) { - this.settings.set_uint("css-last-update", this.cssTag); - return true; + if (!originalCss || !configCss) return false; + // The backup goes next to the user's stylesheet (it went to "undefined.bak" in the + // working directory, #266) + let copyConfigCss = Gio.File.new_for_path(`${configCss.get_path()}.bak`); + try { + let backupFine = this._copyFile(configCss, copyConfigCss); + let copyFine = this._copyFile(originalCss, configCss); + if (backupFine && copyFine) { + this.settings.set_uint("css-last-update", this.cssTag); + return true; + } + } catch (e) { + console.warn(`Forge: can't update the stylesheet ${configCss.get_path()}: ${e.message}`); } } return false; } + /** + * Copies `source` over `target`. The copy gets the usual permissions of a new file, not those + * of `source`: the extension's own files can be read-only (e.g. on NixOS), and a read-only copy + * couldn't be overwritten at the next update (#266). A read-only copy left by an earlier + * version is replaced. + * @param {Gio.File} source + * @param {Gio.File} target + */ + _copyFile(source, target) { + const flags = Gio.FileCopyFlags.OVERWRITE | Gio.FileCopyFlags.TARGET_DEFAULT_PERMS; + try { + return source.copy(target, flags, null, null); + } catch (e) { + // (only a target that exists and is read-only: not when the source can't be read) + const readOnlyTarget = + e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PERMISSION_DENIED) && + target.query_exists(null) && + !target + .query_info("access::can-write", Gio.FileQueryInfoFlags.NONE, null) + .get_attribute_boolean("access::can-write"); + if (!readOnlyTarget) throw e; + target.delete(null); + return source.copy(target, flags, null, null); + } + } + /** * Credits: ExtensionSystem.js:_callExtensionEnable() */