def __init__(self, window: Window, database_file: Gio.File) -> None: super().__init__() filepath = database_file.get_path() self.window = window # Reset headerbar to initial state if it already exists. self.headerbar.title.props.title = database_file.get_basename() self.install_action("clear-keyfile", None, self.on_clear_keyfile) database = self.window.unlocked_db if database: is_current = database.database_manager.path == filepath if is_current: self.database_manager = database.database_manager if not self.database_manager: self.database_manager = DatabaseManager(filepath) if gsecrets.config_manager.get_remember_composite_key(): self._set_last_used_keyfile() if gsecrets.const.IS_DEVEL: self.status_page.props.icon_name = gsecrets.const.APP_ID
def open_local_file(self, file: Gio.File): self._notifications.clear() if file is None: return def error(e): self._notifications.add_simple( _('Cannot load project: ') + str(e), Gtk.MessageType.ERROR) self.buffer.load_from_file(None) ext = os.path.splitext(file.get_basename())[1] if ext != '.tcp': error(Exception(_('Wrong file extenstion'))) return if not file.query_exists(): error( Exception( _('File "{}" is not available').format(file.get_uri()))) return try: self.buffer.load_from_file(file) except lib.CorruptedFileException as e: error(e) except lib.MissingPluginException as e: error(e) except lib.UnavailableCommandException as e: error(e) except GLib.Error as e: error(e)
def on_file_changed(self, monitor: Gio.FileMonitor, file: Gio.File, _other_file: Gio.File, event_type: Gio.FileMonitorEvent, port: int) -> None: if event_type == Gio.FileMonitorEvent.DELETED: logging.info('%s got deleted' % file.get_path()) monitor.disconnect_by_func(self.on_file_changed, port) elif event_type == Gio.FileMonitorEvent.ATTRIBUTE_CHANGED: self.try_replace_root_watcher(monitor, file.get_path(), port)
def set_keyfile(self, keyfile: Gio.File) -> None: self.keyfile_path = keyfile.get_path() keyfile.load_bytes_async(None, self.load_keyfile_callback) self.keyfile_stack.props.visible_child_name = "spinner" self.keyfile_spinner.start() self.keyfile_button.props.sensitive = False logging.debug("Keyfile selected: %s", keyfile.get_path())
def on_file_changed(self, monitor: Gio.FileMonitor, file: Gio.File, _other_file: Gio.File, event_type: Gio.FileMonitorEvent, port: int) -> None: if event_type == Gio.FileMonitorEvent.DELETED: logging.info('%s got deleted' % file.get_path()) if port in self._handlerids: handler_id = self._handlerids.pop(port) monitor.disconnect(handler_id) else: logging.warning(f"No handler id for {port}") elif event_type == Gio.FileMonitorEvent.ATTRIBUTE_CHANGED: self.try_replace_root_watcher(monitor, file.get_path(), port)
def process_passed_image_file(self, chosen_file: Gio.File, content_type: Optional[str] = None): self.reset_result() # The file can be remote, so we should read asynchronously chosen_file.read_async(GLib.PRIORITY_DEFAULT, None, self.cb_file_read, content_type) # If this file is remote, reading it will take time, so we display progress bar. if not chosen_file.is_native(): self.progress_bar.set_visible(True) sid = GLib.timeout_add(100, ui.update_progress, self.progress_bar) # Properly handle GLib event source if self.g_event_sources.get('update_progress'): GLib.Source.remove(self.g_event_sources['update_progress']) self.g_event_sources['update_progress'] = sid
def save_as(self, file: Gio.File) -> bool: """Saves buffer to the file. The file will become current project_file. Args: file (Gio.File): The file Returns: bool: True if the project was saved successfully """ output = [] # Meta-info output.append(('fver', str(FILE_VERSION_FORMAT))) for p in self.enabled_plugins: # Base is enabled by default if p == 'base': continue output.append(('plugin', p)) output.append((('fconsole'), str(self.props.run_in_console))) # Commands output.extend(self.code.save()) self.props.project_file = file outs = file.replace(None, False, Gio.FileCreateFlags.NONE) content = lib.save_tcp(output) ok, bytes_written = outs.write_all(content.encode('utf-8')) if ok: self.props.changed = False return ok
def update_track(self, gloc: Gio.File, force_update: bool = False) -> Optional[trax.Track]: """ Rescan the track at a given location :param gloc: the location :type gloc: :class:`Gio.File` :param force_update: Force update of file (default only updates file when mtime has changed) returns: the Track object, None if it could not be updated """ uri = gloc.get_uri() if not uri: # we get segfaults if this check is removed return None tr = self.collection.get_track_by_loc(uri) if tr: tr.read_tags(force=force_update) else: tr = trax.Track(uri) if tr._scan_valid: self.collection.add(tr) # Track already existed. This fixes trax.get_tracks_from_uri # on windows, unknown why fix isnt needed on linux. elif not tr._init: self.collection.add(tr) return tr
def cb_file_read(self, remote_file: Gio.File, res: Gio.AsyncResult, content_type: Optional[str] = None): w, h = self.get_preview_size() gi_stream: Gio.FileInputStream = remote_file.read_finish(res) scaled_pix = GdkPixbuf.Pixbuf.new_from_stream_at_scale( gi_stream, w, h, True, None) # Prevent freezing GUI Gtk.main_iteration() self.insert_image_to_placeholder(scaled_pix) # Prevent freezing GUI Gtk.main_iteration() gi_stream.seek(0, GLib.SeekType.SET, None) logger.debug('Content type: {}', content_type) if content_type == 'image/svg+xml': svg: Rsvg.Handle = Rsvg.Handle.new_from_stream_sync( gi_stream, remote_file, Rsvg.HandleFlags.FLAGS_NONE, None) stream: io.BytesIO = export_svg(svg) else: stream = io.BytesIO() CHUNNK_SIZE = 8192 # There is no method like read_all_bytes(), so have to do verbose way below while True: buf: GLib.Bytes = gi_stream.read_bytes(CHUNNK_SIZE, None) amount = buf.get_size() logger.debug('Read {} bytes', amount) stream.write(buf.get_data()) if amount <= 0: break if self.g_event_sources.get('update_progress'): GLib.Source.remove(self.g_event_sources['update_progress']) del self.g_event_sources['update_progress'] ui.update_progress(self.progress_bar, 1) self.process_passed_rgb_image(stream)
def get_gicon_for_file(uri): """ Return a GIcon representing the file at the @uri, which can be *either* and uri or a path return None if not found """ gfile = File.new_for_path(uri) if not gfile.query_exists(): gfile = File.new_for_uri(uri) if not gfile.query_exists(): return None finfo = gfile.query_info(FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, None) gicon = finfo.get_attribute_object(FILE_ATTRIBUTE_STANDARD_ICON) return gicon
def _file_changed(self, _monitor, main_file: Gio.File, other_file: Optional[Gio.File], event_type: Gio.FileMonitorEvent) -> None: file_path = main_file.get_path() other_path = (Path(normalize_path(other_file.get_path(), True)) if other_file else None) print_d(f"Got event {event_type} on {file_path}->{other_path}") self.changed.append((event_type, file_path))
def _open(self, folder: Gio.File): """ Open a folder. """ children = folder.enumerate_children( ','.join([Gio.FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, Gio.FILE_ATTRIBUTE_STANDARD_IS_BACKUP, Gio.FILE_ATTRIBUTE_STANDARD_NAME, FileItem.FILE_ATTRIBUTES]), Gio.FileQueryInfoFlags.NONE, None) self._path_bar.location = folder del self._files[:] for info in children: if self._show_hidden or not (info.get_is_hidden() or info.get_is_backup()): self._files.append(FileItem(folder.get_child(info.get_name()), info)) list.sort(self._files, key=attrgetter('name_key'))
def load_from_file(self, file: Gio.File): if file is not None: # Reads the file file_dis = Gio.DataInputStream.new(file.read()) source = file_dis.read_upto('\0', 1)[0] file_dis.close() # Parses the file project = lib.parse_tcp(source) else: project = DEFAULT_PROJECT.copy() # Reset variables self.props.run_in_console = False self.props.project_file = file # Load project # project_code contains only commands (without meta-info like plugin) error = None project_code = [] # IDs of plugins that are requested to load plugin_ids = set(['base']) file_version = 0 for cmd in project: if len(cmd) != 2: error = CorruptedFileException() continue id = cmd[0] data = cmd[1] if id == 'plugin': plugin_id = lib.Plugin.get_id_from_path(data) plugin_ids.add(plugin_id) elif id == 'fver': file_version = int(data) # Skips to conversion from Turtlico 0.x projects if file_version <= 1: break elif id == 'fconsole': self.props.run_in_console = data == 'True' else: project_code.append(cmd) if file_version <= 1: error = None project_code, plugins, self.props.run_in_console = ( legacy.tcp_1_to_2(source, file_version)) plugin_ids.clear() plugin_ids.update(plugins) if error is not None: raise error enabled_plugin_paths = lib.Plugin.resolve_paths_from_ids(plugin_ids) self._reload_plugins(enabled_plugin_paths) self.code.load(project_code) self.props.changed = False
def downloadPixbufAlbumCover(url): if debug: print("downloading album cover from " + url) f = File.new_for_uri(url) stream = f.read() cover = Pixbuf.new_from_stream(stream) stream.close() return cover
def recursive_tracks_from_file(gfile: Gio.File) -> Iterable[Track]: """ Get recursive tracks from Gio.File If it's a directory, expands Gets only valid tracks """ ftype = gfile.query_info('standard::type', Gio.FileQueryInfoFlags.NONE, None).get_file_type() if ftype == Gio.FileType.DIRECTORY: file_infos = gfile.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, None) files = (gfile.get_child(fi.get_name()) for fi in file_infos) for sub_gfile in files: for i in recursive_tracks_from_file(sub_gfile): yield i else: uri = gfile.get_uri() if is_valid_track(uri): yield Track(uri)
def generate_keyfile_sync(gfile: Gio.File) -> str: """The callback returns a GFile as its source object and the keyfile hash as its user_data. Returns the hash of the keyfile.""" key = get_random_bytes(32) cipher = AES.new(key, AES.MODE_EAX) ciphertext, tag = cipher.encrypt_and_digest( create_random_data(96)) # type: ignore contents = cipher.nonce + tag + ciphertext # type: ignore keyfile_hash = GLib.compute_checksum_for_data(GLib.ChecksumType.SHA1, contents) gfile.replace_contents( contents, None, False, Gio.FileCreateFlags.REPLACE_DESTINATION, None, ) return keyfile_hash
def _monitor_handler( self, monitor: Gio.FileMonitor, child: Gio.File, other_file: Gio.File, event_type: Gio.FileMonitorEvent) -> None: # The final path component contains the snapshot number. try: number = int(pathlib.Path(child.get_path()).name) except ValueError: return bootenv = self._bootenvs / str(number) if Gio.FileMonitorEvent.DELETED == event_type and bootenv.exists(): self._clean_up(number)
def trash_or_confirm(gfile: Gio.File) -> bool: """Trash or delete the given Gio.File Files and folders will be moved to the system Trash location without confirmation. If they can't be trashed, then the user is prompted for an irreversible deletion. :rtype: bool :returns: whether the file was deleted """ try: gfile.trash(None) return True except GLib.GError as e: # Handle not-supported, as that's due to the trashing target # being a (probably network) mount-point, not an underlying # problem. We also have to handle the generic FAILED code # because that's what we get with NFS mounts. expected_error = (e.code == Gio.IOErrorEnum.NOT_SUPPORTED or e.code == Gio.IOErrorEnum.FAILED) if not expected_error: raise RuntimeError(str(e)) file_type = gfile.query_file_type(Gio.FileQueryInfoFlags.NONE, None) if file_type == Gio.FileType.DIRECTORY: raise RuntimeError(_("Deleting remote folders is not supported")) elif file_type != Gio.FileType.REGULAR: raise RuntimeError(_("Not a file or directory")) delete_permanently = modal_dialog( primary=_("“{}” can’t be put in the trash. Do you want to " "delete it immediately?".format( GLib.markup_escape_text(gfile.get_parse_name()))), secondary=_("This remote location does not support sending items " "to the trash."), buttons=[ (_("_Cancel"), Gtk.ResponseType.CANCEL), (_("_Delete Permanently"), Gtk.ResponseType.OK), ], ) if delete_permanently != Gtk.ResponseType.OK: return False try: gfile.delete(None) # TODO: Deleting remote folders involves reimplementing # shutil.rmtree for gio, and then calling # self.recursively_update(). except Exception as e: raise RuntimeError(str(e)) return True
def _replace_async_cb(self, gfile: Gio.File, result: Gio.AsyncResult, *user_data): try: self._outputstream = gfile.replace_finish(result) except GLib.Error as e: self._error_message = e.message self._abort() return # The file is now open for writing -> start copying data from the inputstream self._inputstream.read_bytes_async( count=block_size, io_priority=GLib.PRIORITY_DEFAULT, cancellable=self._cancellable, callback=self._read_bytes_async_cb, )
def _create_flatpak_installation(self, remote, target_path): install_path = File.new_for_path(target_path) installation = Installation.new_for_path(install_path, False, None) installation.add_remote(remote, False, None) return installation
def trash_or_confirm(gfile: Gio.File) -> bool: """Trash or delete the given Gio.File Files and folders will be moved to the system Trash location without confirmation. If they can't be trashed, then the user is prompted for an irreversible deletion. :rtype: bool :returns: whether the file was deleted """ try: gfile.trash(None) return True except GLib.GError as e: # Handle not-supported, as that's due to the trashing target # being a (probably network) mount-point, not an underlying # problem. We also have to handle the generic FAILED code # because that's what we get with NFS mounts. expected_error = ( e.code == Gio.IOErrorEnum.NOT_SUPPORTED or e.code == Gio.IOErrorEnum.FAILED ) if not expected_error: raise RuntimeError(str(e)) file_type = gfile.query_file_type( Gio.FileQueryInfoFlags.NONE, None) if file_type == Gio.FileType.DIRECTORY: raise RuntimeError(_("Deleting remote folders is not supported")) elif file_type != Gio.FileType.REGULAR: raise RuntimeError(_("Not a file or directory")) delete_permanently = modal_dialog( primary=_( "“{}” can’t be put in the trash. Do you want to " "delete it immediately?".format( GLib.markup_escape_text(gfile.get_parse_name())) ), secondary=_( "This remote location does not support sending items " "to the trash." ), buttons=[ (_("_Cancel"), Gtk.ResponseType.CANCEL), (_("_Delete Permanently"), Gtk.ResponseType.OK), ], ) if delete_permanently != Gtk.ResponseType.OK: return False try: gfile.delete(None) # TODO: Deleting remote folders involves reimplementing # shutil.rmtree for gio, and then calling # self.recursively_update(). except Exception as e: raise RuntimeError(str(e)) return True
def __file_changed(self, _monitor, main_file: Gio.File, other_file: Optional[Gio.File], event: Gio.FileMonitorEvent) -> None: if event == Event.CHANGES_DONE_HINT: # This seems to work fine on most Linux, but not on Windows / macOS # Or at least, not in CI anyway. # So shortcut the whole thing return try: file_path = main_file.get_path() if file_path is None: return file_path = normalize_path(file_path, True) song = self.get(file_path) file_path = Path(file_path) other_path = (Path(normalize_path(other_file.get_path(), True)) if other_file else None) if event in (Event.CREATED, Event.MOVED_IN): if file_path.is_dir(): self.monitor_dir(file_path) copool.add(self.scan, [str(file_path)]) elif not song: print_d(f"Auto-adding created file: {file_path}", self._name) self.add_filename(file_path) elif event == Event.RENAMED: if not other_path: print_w(f"No destination found for rename of {file_path}", self._name) if song: print_d(f"Moving {file_path} to {other_path}...", self._name) if self.move_song(song, str(other_path)): # type:ignore print_w(f"Song {file_path} has gone") elif self.is_monitored_dir(file_path): if self.librarian: print_d(f"Moving tracks from {file_path} -> {other_path}...", self._name) copool.add(self.librarian.move_root, str(file_path), str(other_path), write_files=False, priority=GLib.PRIORITY_DEFAULT) self.unmonitor_dir(file_path) if other_path: self.monitor_dir(other_path) else: print_w(f"Seems {file_path} is not a track (deleted?)", self._name) # On some (Windows?) systems CHANGED is called which can remove # before we get here, so let's try adding the new path back self.add_filename(other_path) elif event == Event.CHANGED: if song: # QL created (or knew about) this one; still check if it changed if not song.valid(): self.reload(song) else: print_d(f"Auto-adding new file: {file_path}", self._name) self.add_filename(file_path) elif event in (Event.MOVED_OUT, Event.DELETED): if song: print_d(f"...so deleting {file_path}", self._name) self.reload(song) else: # either not a song, or a song that was renamed by QL if self.is_monitored_dir(file_path): self.unmonitor_dir(file_path) # And try to remove all songs under that dir. Slowly. gone = set() for key, song in self.iteritems(): if file_path in Path(key).parents: gone.add(song) if gone: print_d(f"Removing {len(gone)} contained songs in {file_path}", self._name) actually_gone = self.remove(gone) if gone != actually_gone: print_w(f"Couldn't remove all: {gone - actually_gone}", self._name) else: print_d(f"Unhandled event {event} on {file_path} ({other_path})", self._name) return except Exception: print_w("Failed to run file monitor callback", self._name) print_exc() print_d(f"Finished handling {event}", self._name)
def guess_content_type(file: Gio.File) -> str: info: Gio.FileInfo = file.query_info( Gio.FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, Gio.FileQueryInfoFlags.NONE, None) return info.get_attribute_as_string( Gio.FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE)
def on_file_selected(self, button: Gtk.Button, pane: int, file: Gio.File) -> None: path = file.get_path() self.set_location(path)