def _on_special_file_event(self, path, event_type, new_path): logger.debug("Special file event obtained for path %s, type %s", path, event_type) special_dirname, special_filename = op.split(path) filename = special_filename.rsplit('.', 1)[0] # cut off '.download' if not self._is_deleting and self._current_share_name == filename and ( event_type in (DELETE, MOVE) or not op.exists(path)): to_cancel = True special_dirname = ensure_unicode(special_dirname) rel_special_dir = self._relpath(special_dirname) with self._special_event_lock: self._special_event_no += 1 if event_type == MOVE: new_dir, new_file = op.split(new_path) if special_filename == new_file: # folder moved locally new_dir = ensure_unicode(new_dir) rel_new_dir = self._relpath(new_dir) self._sync.update_special_paths.emit( rel_special_dir, rel_new_dir) self._change_folder_uuid_local(new_dir) self._update_spec_files(new_path, self._is_folder) to_cancel = False if event_type == DELETE or to_cancel: folder_deleted = not op.isdir(special_dirname) folder_excluded = self._sync.is_dir_excluded(rel_special_dir) self.cancel_share_download(filename, folder_deleted=folder_deleted, folder_excluded=folder_excluded)
def get_url(): filename = config.get_main_option("filename") if filename is None: filename = ensure_unicode( join(get_patches_dir(get_data_dir(), create=True), 'patches.db')) url = config.get_main_option("sqlalchemy.url") url = ensure_unicode(url) url = url.format(filename=FilePath(filename)) return url
def show_tray_notification(self, text, title=""): if not title: title = tr('Pvtbox') # Convert strings to unicode if type(text) in (str, str): text = ensure_unicode(text) if type(title) in (str, str): title = ensure_unicode(title) logger.info("show_tray_notification: %s, title: %s", text, title) if self._tray.supportsMessages(): self._tray.showMessage(title, text) else: logger.warning("tray does not supports messages")
def __init__(self, cfg, web_api, filename, ss_client, tracker=None, parent=None, network_speed_calculator=None, db=None): """ Constructor @param web_api Client_API class instance [Client_API] @param ss_client Instance of signalling.SignalServerClient @param tracker Instance of stat_tracking.Tracker """ QObject.__init__(self, parent=parent) self._cfg = cfg # Client_API class instance self._web_api = web_api # Signalling server client instance self._ss_client = ss_client self._tracker = tracker self._network_speed_calculator = network_speed_calculator self._db = db self._filename = ensure_unicode(filename) self.task_to_report = {} self._downloader = HttpDownloader( network_speed_calculator=self._network_speed_calculator) self._set_callbacks() # Download tasks info as task_id: info self.download_tasks_info = {} self._last_progress_report_time = 0 self._last_length = 0.0 self._was_stopped = False self._empty_progress = (None, 0, 0) self._open_uploads_file() self._uploads_deleted = \ self._uploads_excluded = \ self._uploads_not_synced = set() self._on_server_connect_signal.connect(self._on_server_connect) self._upload_task_completed_signal.connect( self._on_upload_task_completed) self._upload_task_error_signal.connect(self._on_upload_task_error) self._upload_task_progress_signal.connect( self._on_upload_task_progress) self._on_upload_added_signal.connect(self._on_upload_added) self._on_upload_cancel_signal.connect(self._on_upload_cancel) self._check_upload_path_timer = QTimer(self) self._check_upload_path_timer.setInterval(10 * 1000) self._check_upload_path_timer.timeout.connect(self._check_upload_paths)
def _get_icons_info(self, file_list, added, removed): icons_info = [] try: for path, is_dir, created_time, was_updated in file_list: if path not in added: icons_info.append((None, None, None)) continue elif path in self._files: icons_info.append(self._files[path]) continue abs_path = op.join(self._config.sync_directory, path) abs_path = ensure_unicode(abs_path) abs_path = FilePath(abs_path) # .longpath doesn't work mime, _ = mimetypes.guess_type(path) image = QImage(abs_path) if image.isNull(): image = None icons_info.append((image, mime, QFileInfo(abs_path))) except Exception as e: logger.error("Icons info error: %s", e) self._icons_info_ready.emit( icons_info, file_list, added, removed)
def start(self): self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._factory = WebSocketServerFactory( "ws://127.0.0.1", loop=self._loop) self._factory.protocol = IPCWebSocketProtocol coro = self._factory.loop.create_server(self._factory, '127.0.0.1', 0) self._server = self._factory.loop.run_until_complete(coro) _, port = self._server.sockets[0].getsockname() self._factory.setSessionParameters(url="ws://127.0.0.1:{}".format(port)) if get_platform() == 'Darwin': port_path = join( HOME_DIR, 'Library', 'Containers', 'net.pvtbox.Pvtbox.PvtboxFinderSync', 'Data', 'pvtbox.port') else: port_path = join( get_cfg_dir(), 'pvtbox.port') port_path = ensure_unicode(port_path) self._loop.call_soon_threadsafe( self._write_opened_port_to_accessible_file, port, port_path) if get_platform() == 'Darwin': port_path = join( HOME_DIR, 'Library', 'Containers', 'net.pvtbox.Pvtbox.PvtboxShareExtension', 'Data', 'pvtbox.port') port_path = ensure_unicode(port_path) self._loop.call_soon_threadsafe( self._write_opened_port_to_accessible_file, port, port_path) self._factory.loop.run_forever()
def get_shared_reply(): paths = params.get_shared_paths_func() if paths is None: return None paths = list( map( lambda path: FilePath( ensure_unicode(join(params.cfg.sync_directory, path))). longpath, paths)) cmd = dict(cmd="shared", paths=paths) return json.dumps(cmd)
def _relpath(self, path, data_dir=None): rel_path = None if not data_dir: data_dir = FilePath( self._cfg.sync_directory if self._cfg else get_data_dir()) if not path: path = FilePath(data_dir) if path in data_dir: rel_path = op.relpath(path, data_dir) if rel_path == os.curdir: rel_path = "" rel_path = ensure_unicode(rel_path) return rel_path
def get_offline_status(paths): if not params.cfg.smart_sync: return 2 # no smart sync rel_paths = list() for path in paths: path = ensure_unicode(path) try: # Name of the file relative to the root directory root, rel_path = get_relpath(path) if not rel_path: raise SharePathException() rel_paths.append(rel_path) except SharePathException: logger.warning("Incorrect path %s", path) return 2 # no smart sync return params.sync.get_offline_status(rel_paths, timeout=0.5)
def file_event_create(self, event_uuid, file_name, file_size, folder_uuid, diff_file_size, file_hash): ''' 'file_event_create' request of file event registration API. Registers file creation on API server @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = { "event_uuid": event_uuid, "file_name": ensure_unicode(file_name), "file_size": file_size, "folder_uuid": folder_uuid if folder_uuid else "", "diff_file_size": diff_file_size, "hash": file_hash, } return self.create_event_request(action='file_event_create', data=data)
def _update_file_list_item_widget(self, widget, rel_path, created_time, was_updated, icon_info): abs_path = op.join(self._config.sync_directory, rel_path) abs_path = ensure_unicode(abs_path) abs_path = FilePath(abs_path).longpath widget.created_time = created_time widget.was_updated = was_updated widget.file_name_label.setText( elided(rel_path, widget.file_name_label)) widget.time_delta_label.setText(get_added_time_string( created_time, was_updated, False)) self._set_icon_label(icon_info, widget.icon_label) get_link_button = widget.get_link_button get_link_button.rel_path = rel_path get_link_button.abs_path = abs_path if self._is_shared(rel_path): if not get_link_button.icon(): get_link_button.setIcon(QIcon(':images/getLink.png')) elif get_link_button.icon(): get_link_button.setIcon(QIcon())
def _start_service(self): if not self._starting_service: self._starting_service = True self._stop_service() logger.debug("Starting service...") options = dict(shell=True) \ if is_launched_from_code() else dict(shell=False) platform = get_platform() if platform == 'Darwin': options['close_fds'] = True from_code = is_launched_from_code() if not from_code: self._args = map(lambda a: a.strip('"'), self._args) args = get_service_start_command() + \ list(map(lambda a: ensure_unicode(a), self._args)) cmd = ' '.join(args) if from_code else \ list(args) if "--logging-disabled" not in self._args: self._reopen_stderr_log() else: self._stderr_log = None logger.debug("Service start command: %s", cmd) self._service_process = subprocess.Popen( cmd, stderr=self._stderr_log, **options) if not self._start_only: self._loop.call_later( self.start_service_timeout, self._drop_starting_service) if self._starting_service_signal: self._starting_service_signal.emit() if not self._start_only: self._loop.call_later( self.connect_to_service_interval, self._connect_to_service)
def _on_upload_task_completed(self, upload_id_str, elapsed, total_str): """ Slot to be called on upload task download completion @param upload_id_str ID of upload task [string] @param elapsed Time elapsed from download starting (in seconds) [float] @param total_str Size of file being downloaded (in bytes) [string] """ upload_id = int(upload_id_str) state = self.download_tasks_info[upload_id]['state'] upload_name = self.download_tasks_info[upload_id]['upload_name'] if state == 'cancelled': logger.debug("Upload task %s cancelled", upload_id) self._on_upload_failed(upload_id) # Tray notification self.upload_cancelled.emit(upload_name) return elif state == 'paused': self.download_tasks_info[upload_id]['elapsed'] += elapsed return elapsed += self.download_tasks_info[upload_id]['elapsed'] total = int(total_str) bps_avg = int(total / elapsed) if elapsed > 0 else 0 bps_avg = "{:,}".format(bps_avg) logger.info( "Upload task ID '%s' complete (downloaded %s bytes in %s seconds" "(%s Bps))", upload_id_str, total_str, elapsed, bps_avg) # Calculate checksum tmp_fn = self.download_tasks_info[upload_id]['tmp_fn'] checksum = self.download_tasks_info[upload_id]['upload_md5'] try: logger.debug("Calculating checksum for upload task ID '%s'...", upload_id) checksum_calculated = hashfile(tmp_fn) except Exception as e: logger.error("Failed to calculate checksum of '%s' (%s)", tmp_fn, e) self._on_upload_failed(upload_id) return if self._tracker: self._tracker.http_download(upload_id, total, elapsed, checksum_calculated == checksum) # Validate checksum if checksum_calculated != checksum: logger.error("MD5 checkfum of '%s' is '%s' instead of '%s'", tmp_fn, checksum_calculated, checksum) self._on_upload_failed(upload_id) return # Move file to its location path = self._check_upload_path(upload_id) if path is None: return path = FilePath(op.join(path, upload_name)) fullpath = ensure_unicode(op.join(self._cfg.sync_directory, path)) fullpath = FilePath(fullpath).longpath dirname = op.dirname(fullpath) if not op.isdir(dirname): logger.warning( "Destination directory %s" "does not exist for upload %s", dirname, fullpath) self._on_upload_failed(upload_id) return try: try: logger.info("Moving downloaded file '%s' to '%s'...", tmp_fn, fullpath) # Create necessary directories make_dirs(fullpath) # Move file shutil.move(src=tmp_fn, dst=fullpath) except OSError as e: if e.errno != errno.EACCES: raise e logger.warning( "Can't move downloaded file '%s' into '%s' (%s)", tmp_fn, dirname, e) fullpath = get_next_name(fullpath) shutil.move(src=tmp_fn, dst=fullpath) except Exception as e: logger.error("Failed to move downloaded file '%s' into '%s' (%s)", tmp_fn, dirname, e) self._on_upload_failed(upload_id) return self.download_status.emit(*self._empty_progress, [{}, {}, [upload_id_str]], {}) self._on_upload_complete(upload_id)
def _on_sync_folder_location_button_clicked(self): selected_folder = QFileDialog.getExistingDirectory( self._dialog, tr('Choose Pvtbox folder location'), get_parent_dir(FilePath(self._cfg.sync_directory))) selected_folder = ensure_unicode(selected_folder) try: if not selected_folder: raise self._MigrationFailed("Folder is not selected") if len(selected_folder + "/Pvtbox") > self._max_root_len: if not self._migrate: msgbox(tr("Destination path too long. " "Please select shorter path."), tr("Path too long"), parent=self._dialog) raise self._MigrationFailed("Destination path too long") free_space = get_free_space(selected_folder) selected_folder = get_data_dir(dir_parent=selected_folder, create=False) if FilePath(selected_folder) == FilePath(self._cfg.sync_directory): raise self._MigrationFailed("Same path selected") if FilePath(selected_folder) in FilePath(self._cfg.sync_directory): msgbox(tr("Can't migrate into existing Pvtbox folder.\n" "Please choose other location"), tr("Invalid Pvtbox folder location"), parent=self._dialog) raise self._MigrationFailed( "Can't migrate into existing Pvtbox folder") if self._size and free_space < self._size: logger.debug( "No disk space in %s. Free space: %s. Needed: %s.", selected_folder, free_space, self._size) msgbox(tr( "Insufficient disk space for migration to\n{}.\n" "Please clean disk", selected_folder), tr("No disk space"), parent=self._dialog) raise self._MigrationFailed( "Insufficient disk space for migration") self._migration_cancelled = False dialog = QProgressDialog(self._dialog) dialog.setWindowTitle(tr('Migrating to new Pvtbox folder')) dialog.setWindowIcon(QIcon(':/images/icon.svg')) dialog.setModal(True) dialog.setMinimum(0) dialog.setMaximum(100) dialog.setMinimumSize(400, 80) dialog.setAutoClose(False) def progress(value): logger.debug("Migration dialog progress received: %s", value) dialog.setValue(value) def migration_failed(error): logger.warning("Migration failed with error: %s", error) msgbox(error, tr('Migration to new Pvtbox folder error'), parent=dialog) dialog.cancel() self._migration_cancelled = True done() def cancel(): logger.debug("Migration dialog cancelled") self._migration_cancelled = True self._migration.cancel() def done(): logger.debug("Migration done") try: self._migration.progress.disconnect(progress) self._migration.failed.disconnect(migration_failed) self._migration.done.disconnect(done) dialog.canceled.disconnect(cancel) except Exception as e: logger.warning("Can't disconnect signal %s", e) dialog.hide() dialog.done(QDialog.Accepted) dialog.close() self._migration = SyncDirMigration(self._cfg, parent=self._dialog) self._migration.progress.connect(progress, Qt.QueuedConnection) self._migration.failed.connect(migration_failed, Qt.QueuedConnection) self._migration.done.connect(done, Qt.QueuedConnection) dialog.canceled.connect(cancel) self._exit_service() old_dir = self._cfg.sync_directory self._migration.migrate(old_dir, selected_folder) def on_finished(): logger.info("Migration dialog closed") if not self._migration_cancelled: logger.debug("Setting new location") self._ui.location_edit.setText(FilePath(selected_folder)) disable_file_logging(logger) shutil.rmtree(op.join(old_dir, '.pvtbox'), ignore_errors=True) set_root_directory(FilePath(selected_folder)) enable_file_logging(logger) make_dir_hidden(get_patches_dir(selected_folder)) self._start_service() dialog.finished.connect(on_finished) dialog.show() except self._MigrationFailed as e: logger.warning("Sync dir migration failed. Reason: %s", e) finally: if self._migrate: self._dialog.accept()
def _create_file_list_item_widget(self, rel_path, created_time, was_updated, icon_info): abs_path = op.join(self._config.sync_directory, rel_path) abs_path = ensure_unicode(abs_path) abs_path = FilePath(abs_path).longpath widget = QWidget(parent=self._ui.file_list) widget.created_time = created_time widget.was_updated = was_updated widget.mouseReleaseEvent = lambda _: \ qt_reveal_file_in_file_manager( widget.get_link_button.abs_path) main_layout = QHBoxLayout(widget) icon_label = QLabel(widget) widget.icon_label = icon_label main_layout.addWidget(icon_label) vertical_layout = QVBoxLayout() main_layout.addLayout(vertical_layout) file_name_label = QLabel(widget) widget.file_name_label = file_name_label vertical_layout.addWidget(file_name_label) horizontal_layout = QHBoxLayout() vertical_layout.addLayout(horizontal_layout) time_delta_label = QLabel(widget) widget.time_delta_label = time_delta_label horizontal_layout.addWidget(time_delta_label, alignment=Qt.AlignTop) get_link_button = QPushButton(widget) widget.get_link_button = get_link_button horizontal_layout.addWidget(get_link_button, alignment=Qt.AlignTop) self._set_icon_label(icon_info, icon_label) file_name_label.setFixedSize(304, 24) file_name_label.setFont(QFont('Noto Sans', 10 * self._dp)) file_name_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) file_name_label.setText( elided(rel_path, file_name_label)) time_delta_label.setText(get_added_time_string( created_time, was_updated, False)) time_delta_label.setFont(QFont('Noto Sans', 8 * self._dp, italic=True)) time_delta_label.setMinimumSize(time_delta_label.width(), 24) time_delta_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) time_delta_label.setStyleSheet('color: #A792A9;') get_link_button.setText(' {} '.format(tr('Get link'))) get_link_button.setFlat(True) get_link_button.setChecked(True) get_link_button.setMinimumSize(120, 24) get_link_button.setFont(QFont("Noto Sans", 8 * self._dp, italic=True)) get_link_button.setMouseTracking(True) self._setup_get_link_button(get_link_button, rel_path, abs_path) return widget
def offline_paths(paths, is_offline=True, is_recursive=True): """ Makes given paths offline as is_offline flag @param paths paths [list] @param is_offline flag [bool] @return None """ def process_error(error, error_info=''): msg = { INCORRECT_PATH: "Failed to change offline status '%s'. Incorrect path", NOT_IN_SYNC: "Path for changing offline status not in sync '%s'", } logger.error(msg[error], paths) if params.tracker: tracker_errors = { INCORRECT_PATH: params.tracker.INCORRECT_PATH, NOT_IN_SYNC: params.tracker.NOT_IN_SYNC, } params.tracker.share_error(0, tracker_errors[error], time.time() - start_time) start_time = time.time() timeout = 10 * 60 # seconds message_timeout = 2 # seconds step = 0 command_str = tr("add to offline") if is_offline \ else tr("remove from offline") for path in paths: path = ensure_unicode(path) try: # Name of the file relative to the root directory root, rel_path = get_relpath(path) if not rel_path: raise SharePathException() except SharePathException: process_error(INCORRECT_PATH) return if rel_path.endswith(FILE_LINK_SUFFIX): rel_path = rel_path[:-len(FILE_LINK_SUFFIX)] if not rel_path: process_error(INCORRECT_PATH) return logger.info("Offline on=%s, path '%s'...", is_offline, rel_path) while True: # Wait if file not in db yet try: if op.isfile(path): uuid = params.sync.get_file_uuid(rel_path) elif op.isdir(path): uuid = params.sync.get_folder_uuid(rel_path) else: process_error(INCORRECT_PATH) return except (FileNotFound, FileInProcessing, FileEventsDBError): uuid = None if uuid or (time.time() - start_time > timeout and node_synced): break if step == message_timeout: filename = op.basename(path) Application.show_tray_notification( tr("Prepare {}.\n" "Action will be completed after {} synced").format( command_str, filename)) step += 1 time.sleep(1) if not uuid: process_error(NOT_IN_SYNC) Application.show_tray_notification( tr("Can't {}.\n" "{} not in sync").format(command_str, path)) return try: params.sync.file_added_to_indexing.emit(FilePath(path)) success = params.sync.make_offline(uuid, is_offline, is_recursive=is_recursive) except FileEventsDBError: success = False if not success: params.sync.file_removed_from_indexing.emit(FilePath(path)) Application.show_tray_notification( tr("Can't {} for path: {}.").format(command_str, path))
def migrate(self, old_dir, new_dir): logger.info("Starting sync dir migration from %s, to %s", old_dir, new_dir) old_dir = FilePath(old_dir).longpath new_dir = FilePath(new_dir).longpath old_files = get_filelist(old_dir) old_dirs = get_dir_list(old_dir) total_count = len(old_files) + len(old_dirs) + 1 progress = 0 sent_progress = 0 logger.debug("Migration progress: %s/%s (%s%%)", 0, total_count, sent_progress) count = 1 copied_dirs = [] copied_files = [] make_dirs(new_dir, is_folder=True) copied_dirs.append(new_dir) logger.debug("Migration progress: %s/%s (%s%%)", count, total_count, sent_progress) self.progress.emit(sent_progress) for dir in old_dirs: if self._cancelled.isSet(): self._delete(dirs=copied_dirs) logger.debug("Migration done because cancelled") self.done.emit() return new_dir_path = ensure_unicode(op.join( new_dir, op.relpath(dir, start=old_dir))) try: make_dirs(new_dir_path, is_folder=True) except Exception as e: logger.error("Make dirs error: %s", e) self.failed.emit(str(e)) self._delete(dirs=copied_dirs) return copied_dirs.append(new_dir_path) count += 1 progress = int(count / total_count * 100) if progress > sent_progress: sent_progress = progress self.progress.emit(sent_progress) logger.debug("Migration progress: %s/%s (%s%%)", count, total_count, sent_progress) for file in old_files: if self._cancelled.isSet(): self._delete(dirs=copied_dirs, files=copied_files) logger.debug("Migration done because cancelled") self.done.emit() return if file in HIDDEN_FILES: continue new_file_path = ensure_unicode(op.join( new_dir, op.relpath(file, start=old_dir))) logger.info("Copying file %s, to %s", file, new_file_path) try: copy_file(file, new_file_path, preserve_file_date=True) except Exception as e: logger.error("Copy file error: %s", e) self.failed.emit(str(e)) self._delete(dirs=copied_dirs, files=copied_files) return copied_files.append(new_file_path) count += 1 progress = int(count / total_count * 100) if progress > sent_progress: sent_progress = progress self.progress.emit(sent_progress) logger.debug("Migration progress: %s/%s (%s%%)", count, total_count, sent_progress) logger.debug("Saving new config") self._cfg.set_settings(dict(sync_directory=FilePath(new_dir))) self._cfg.sync() logger.info("New config saved") logger.debug("Updating shortcuts") create_shortcuts(new_dir) remove_shortcuts(old_dir) logger.debug("Resetting custom folder icons") reset_all_custom_folder_icons(old_dir) logger.debug("Migration done") self.done.emit() logger.info("Migration thread end")
def collaboration_path_settings(paths): """ Prepares opening of collaboration settings dialog for given paths @param paths Path (1 element list) to folder with (potential) collaboration [list] @return None """ def process_error(error, error_info=''): msg = { INCORRECT_PATH: "Failed to open collaboration settings '%s'. Incorrect path", NOT_IN_SYNC: "Path for collaboration settings not in sync '%s'", } logger.error(msg[error], paths) if params.tracker: tracker_errors = { INCORRECT_PATH: params.tracker.INCORRECT_PATH, NOT_IN_SYNC: params.tracker.NOT_IN_SYNC, } params.tracker.share_error( 0, tracker_errors[error], time.time() - start_time) start_time = time.time() timeout = 10 * 60 # seconds message_timeout = 2 # seconds step = 0 if len(paths) != 1: process_error(INCORRECT_PATH) return path = ensure_unicode(paths[0]) if not op.isdir(path): process_error(INCORRECT_PATH) return try: # Name of the file relative to the root directory root, rel_path = get_relpath(path) if not rel_path or '/' in rel_path: raise SharePathException() except SharePathException: process_error(INCORRECT_PATH) return logger.info("Collaboration settings path '%s'...", rel_path) while True: # Wait if file not in db yet try: uuid = params.sync.get_folder_uuid(rel_path) except (FileNotFound, FileInProcessing, FileEventsDBError): uuid = None if uuid or (time.time() - start_time > timeout and node_synced): break if step == message_timeout: filename = op.basename(path) Application.show_tray_notification( tr("Prepare open collaboration settings.\n" "Dialog will be opened after {} synced").format( filename)) step += 1 time.sleep(1) if not uuid: process_error(NOT_IN_SYNC) Application.show_tray_notification( tr("Can't open collaboration settings.\n" "{} not in sync").format(path)) return logger.debug("Collaboration settings requested for path %s, uuid %s", rel_path, uuid) signals.show_collaboration_settings.emit(rel_path, uuid)