From e3dbf39e747e1a8cf98fb4fed5748a42a79ff657 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 21 Jan 2026 21:41:10 +0100 Subject: [PATCH 1/3] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aman Alam Co-authored-by: Hosted Weblate Co-authored-by: Jun Hwi Ku Co-authored-by: gift983 <983649@my.leicestercollege.ac.uk> Co-authored-by: lorduskordus Co-authored-by: 김유빈 Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/ Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ko/ Translation: Pop OS/COSMIC Files --- i18n/cs/cosmic_files.ftl | 8 +-- i18n/ko/cosmic_files.ftl | 135 +++++++++++++++++++++++++++++++++++---- i18n/pa/cosmic_files.ftl | 0 i18n/ti/cosmic_files.ftl | 0 4 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 i18n/pa/cosmic_files.ftl create mode 100644 i18n/ti/cosmic_files.ftl diff --git a/i18n/cs/cosmic_files.ftl b/i18n/cs/cosmic_files.ftl index 06d98ef..eef2118 100644 --- a/i18n/cs/cosmic_files.ftl +++ b/i18n/cs/cosmic_files.ftl @@ -82,10 +82,10 @@ settings = Nastavení ### Appearance appearance = Vzhled -theme = Téma +theme = Motiv match-desktop = Podle systému -dark = Tmavé -light = Světlé +dark = Tmavý +light = Světlý # Context menu add-to-sidebar = Přidat do postranního panelu new-file = Nový soubor... @@ -384,7 +384,7 @@ item-accessed = Poslední přístup: { $accessed } calculating = Vypočítávání... single-click = Otevřít jedním kliknutím type-to-search = Vyhledávání psaním -type-to-search-recursive = Prohledává současnou složku a její podsložky +type-to-search-recursive = Prohledává aktuální složku a její podsložky type-to-search-enter-path = Zadává cestu ke složce nebo souboru compress = Komprimovat eject = Vysunout diff --git a/i18n/ko/cosmic_files.ftl b/i18n/ko/cosmic_files.ftl index b30ed59..43bef70 100644 --- a/i18n/ko/cosmic_files.ftl +++ b/i18n/ko/cosmic_files.ftl @@ -1,19 +1,19 @@ -cosmic-files = 코스믹 파일 +cosmic-files = COSMIC 파일 empty-folder = 빈 폴더 empty-folder-hidden = 빈 폴더 (숨겨진 항목 있음) -filesystem = 파일시스템 +filesystem = 파일 시스템 home = 홈 trash = 휴지통 # New File/Folder Dialog create-new-file = 새 파일 만들기 create-new-folder = 새 폴더 만들기 -file-name = File name -folder-name = Folder name -file-already-exists = 해당 이름을 가진 파일이 이미 존재합니다. -folder-already-exists = 해당 이름을 가진 폴더가 이미 존재합니다. -name-hidden = "." 으로 시작할 시 파일이 숨겨집니다. -name-invalid = Name cannot be "{ $filename }". -name-no-slashes = 파일명에 슬래시를 포함할 수 없습니다. +file-name = 파일 이름 +folder-name = 폴더 이름 +file-already-exists = 같은 이름의 파일이 이미 있습니다 +folder-already-exists = 같은 이름의 폴더가 이미 있습니다 +name-hidden = "." 으로 시작하는 항목은 숨겨집니다 +name-invalid = "{ $filename }"은(는) 사용할 수 없는 이름입니다 +name-no-slashes = 이름에 슬래시(/)를 사용할 수 없습니다 # Open/Save Dialog cancel = 취소 open = 열기 @@ -28,8 +28,8 @@ rename-file = 파일 이름 바꾸기 rename-folder = 폴더 이름 바꾸기 # Replace Dialog replace = 대체 -replace-title = { $filename } (이)가 이 위치에 이미 있습니다. -replace-warning = 이 파일을 다른 파일로 교체하시겠습니까? 이 파일을 대체할 시 파일 내용이 변경됩니다. +replace-title = "{ $filename }" 이(가) 이미 현재 위치에 있습니다 +replace-warning = 저장하려는 파일로 해당 항목을 대체할까요? 대체 시 내용을 덮어쓰게 됩니다. # List view name = 이름 modified = 수정된 날짜 @@ -44,7 +44,7 @@ size = 크기 ## Operations pending = 진행 중 -failed = 실패 +failed = 실패함 complete = 완료 ## Open with @@ -61,13 +61,13 @@ settings = 설정 ### Appearance -appearance = 모양새 +appearance = 외관 theme = 테마 match-desktop = 데스크톱에 맞춤 dark = 다크 light = 라이트 # Context menu -new-file = 새 파일 +new-file = 새 파일... new-folder = 새 폴더 open-in-terminal = 터미널에서 열기 move-to-trash = 휴지통으로 이동 @@ -103,3 +103,110 @@ grid-view = 그리드 보기 list-view = 목록 보기 menu-settings = 설정... menu-about = 코스믹 파일에 대하여... +connect = 연결 +read-execute = 읽기 및 실행 +item-modified = 마지막 수정 일자: { $modified } +dismiss = 메시지 무시 +copy_noun = 복사 +progress = { $percent }% +related-apps = 관련 앱 +compress = 압축 +network-drive-error = 네트워크 드라이브에 접근할 수 없음 +icon-size-and-spacing = 아이콘 크기 및 간격 +password = 암호 +type-to-search-enter-path = 폴더 혹은 파일의 경로 입력 +emptying-trash = { trash } 비우는 중 ({ $progress })... +trashed-on = 버려짐 +remove = 제거 +original-file = 원본 파일 +create = 생성 +create-archive = 기록 생성 +read-write-execute = 읽기, 쓰기 및 실행 +other-apps = 다른 앱 +set-permissions = "{ $name }"의 권한을 { $mode }로 설정함 +pause = 정지 +calculating = 계산 중... +keep = 유지 +item-size = 크기: { $size } +connecting = 연결 중... +read-write = 읽기 및 쓰기 +none = 없음 +items = 항목: { $items } +no-results = 결과 없음 +type = 형식: { $mime } +resume = 재개 +remember-password = 암호 저장 +username = 사용자 이름 +show-details = 세부 사항 표시 +extract-to = 다른 위치에 압축 해제... +add-network-drive = 네트워크 드라이브 추가 +delete = 삭제 +repository = 저장소 +replace-warning-operation = 해당 항목을 대체할까요? 대체 시 내용을 덮어쓰게 됩니다. +support = 지원 +try-again = 다시 시도 +eject = 꺼내기 +other = 기타 +open-in-new-window = 새 창에서 열기 +read-only = 읽기 전용 +browse-store = { $store } 둘러보기 +enter-server-address = 서버 주소 입력 +connect-anonymously = 익명으로 연결 +group = 그룹 +apply-to-all = 모두 적용 +skip = 건너뛰기 +replace-with = 대체할 파일 +recents = 최근 +network-drive-description = + 서버 주소는 프로토콜 접두어와 주소를 포함해야 합니다. + 예시: ssh://192.168.0.1, ftp://[2001:db8::1] +single-click = 클릭 한 번으로 열기 +undo = 되돌리기 +setting-permissions = "{ $name }"의 권한을 { $mode }로 설정 중 +owner = 소유자 +creating = "{ $parent }"에 "{ $name }" 생성 중 +execute-only = 실행 전용 +open-item-location = 항목 위치 열기 +details = 세부 정보 +mounted-drives = 마운트된 드라이브 +mount-error = 드라이브에 접근할 수 없음 +extract-here = 압축 해제 +removed-from-recents = { recents }에서 { $items } 항목 제거됨 +add-to-sidebar = 사이드 바에 추가 +item-created = 생성 일자: { $created } +type-to-search-recursive = 현재 폴더와 하위 폴더 탐색 +history = 기록 +progress-paused = { $percent }%, 정지됨 +desktop-view-options = 바탕화면 표시 설정... +show-on-desktop = 바탕화면에 표시 +cancelled = 취소됨 +domain = 도메인 +edit-history = 기록 수정 +progress-failed = { $percent }%, 실패함 +item-accessed = 마지막 접근 일자: { $accessed } +extract-to-title = 폴더로 압축 해제 +open-with = 다음으로 열기 +keep-both = 둘 다 유지 +icon-size = 아이콘 크기 +open-with-title = "{ $name }"을(를) 어떻게 열까요? +write-execute = 쓰기 및 실행 +extract-password-required = 암호 필요 +desktop-folder-content = 바탕화면 폴더 내용 +no-history = 기록된 항목이 없습니다. +emptied-trash = { trash } 비워짐 +progress-cancelled = { $percent }%, 취소됨 +open-in-new-tab = 새 탭에서 열기 +unknown-folder = 알 수 없는 폴더 +created = "{ $parent }"에 "{ $name }" 생성됨 +delete-permanently = 완전히 삭제 +networks = 네트워크 +write-only = 쓰기 전용 +today = 오늘 +permanently-delete-warning = { $target }이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다. +empty-trash-warning = 휴지통의 항목이 완전히 삭제됩니다 +empty-trash = 휴지통 비우기 +empty-trash-title = 휴지통을 비울까요? +type-to-search = 입력하여 검색 +notification-in-progress = 파일 작업이 진행 중입니다 +permanently-delete-question = 완전히 삭제할까요? +selected-items = { $items }개 항목 선택됨 diff --git a/i18n/pa/cosmic_files.ftl b/i18n/pa/cosmic_files.ftl new file mode 100644 index 0000000..e69de29 diff --git a/i18n/ti/cosmic_files.ftl b/i18n/ti/cosmic_files.ftl new file mode 100644 index 0000000..e69de29 From 1963e585608338bbd9a7bcd42015f28eee09436c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:32:26 +0100 Subject: [PATCH 2/3] perf(copy): async batch file flushes Instead of calling `sync_all()` on every file individually during the Copy operation, the flushing is now batched and done at the end. Flushing now also happens for Move. --- src/operation/recursive.rs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index 4c19bc3..b4825e9 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -1,6 +1,7 @@ use compio::BufResult; use compio::buf::{IntoInner, IoBuf}; use compio::io::{AsyncReadAt, AsyncWriteAt}; +use futures::{StreamExt, stream}; use std::future::Future; use std::pin::Pin; use std::time::Instant; @@ -57,6 +58,8 @@ impl Context { ) -> Result { let mut ops = Vec::new(); let mut cleanup_ops = Vec::new(); + let mut written_files = Vec::new(); + let mut target_dirs = std::collections::HashSet::new(); for (from_parent, to_parent) in from_to_pairs { self.controller .check() @@ -141,6 +144,9 @@ impl Context { cleanup_ops.push(cleanup_op); } } + if let Some(parent) = op.to.parent() { + target_dirs.insert(parent.to_path_buf()); + } ops.push(op); } @@ -177,10 +183,19 @@ impl Context { &self.controller, ) })? { + if matches!( + op.kind, + OpKind::Copy + | OpKind::Move { + cross_device_copy: true + } + ) { + written_files.push(op.to.clone()); + } // The from path is ignored in the operation selection if it is a top level item if self.op_sel.ignored.contains(&op.from) { // So add the to path to the selection - self.op_sel.selected.push(op.to.clone()); + self.op_sel.selected.push(op.to); } } else { // Cancelled @@ -188,6 +203,22 @@ impl Context { } } + // Sync files to disk + let file_stream = stream::iter(written_files.into_iter().map(|path| async move { + if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await { + let _ = file.sync_all().await; + } + })); + file_stream.buffer_unordered(32).collect::>().await; + + // Sync directories to disk + let dir_stream = stream::iter(target_dirs.into_iter().map(|path| async move { + if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await { + let _ = dir.sync_all().await; + } + })); + dir_stream.buffer_unordered(16).collect::>().await; + Ok(true) } @@ -411,8 +442,6 @@ impl Op { } } } - - to_file.sync_all().await?; } OpKind::Move { cross_device_copy } => { // Remove `to` if overwriting and it is an existing file From d9f654ffe329198bbfb66375d6b923009d28e5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:48:31 +0100 Subject: [PATCH 3/3] improv(zip_extract): flush files to disk This ensures the files are fully on the disk when the operation is done. --- src/archive.rs | 108 ++++++++++++++++++------------------- src/operation/mod.rs | 23 ++++++++ src/operation/recursive.rs | 20 ++----- 3 files changed, 79 insertions(+), 72 deletions(-) diff --git a/src/archive.rs b/src/archive.rs index 5f0fe63..8684bb8 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -1,15 +1,14 @@ -use std::{ - collections::VecDeque, - fs, - io::{self, Read, Write}, - path::Path, -}; -use zip::result::ZipError; - use crate::{ mime_icon::mime_for_path, - operation::{Controller, OpReader, OperationError, OperationErrorType}, + operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}, }; +use std::{ + collections::HashSet, + fs, + io::{self, Read, Write}, + path::{Path, PathBuf}, +}; +use zip::result::ZipError; pub const SUPPORTED_ARCHIVE_TYPES: &[&str] = &[ "application/gzip", @@ -113,27 +112,36 @@ fn zip_extract>( use std::{ffi::OsString, fs}; use zip::result::ZipError; - fn make_writable_dir_all>(outpath: T) -> Result<(), ZipError> { - fs::create_dir_all(outpath.as_ref())?; + fn make_writable_dir_all>( + outpath: T, + target_dirs: &mut HashSet, + ) -> Result<(), ZipError> { + let path = outpath.as_ref(); + if !path.exists() { + fs::create_dir_all(path)?; + } + if !target_dirs.contains(path) { + target_dirs.insert(path.to_path_buf()); + } + #[cfg(unix)] { // Dirs must be writable until all normal files are extracted use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions( - outpath.as_ref(), - std::fs::Permissions::from_mode( - 0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(), - ), + fs::set_permissions( + path, + fs::Permissions::from_mode(0o700 | fs::metadata(path)?.permissions().mode()), )?; } Ok(()) } - #[cfg(unix)] - let mut files_by_unix_mode = Vec::new(); let mut buffer = vec![0; 4 * 1024 * 1024]; let total_files = archive.len(); - let mut pending_directory_creates = VecDeque::new(); + let mut written_files = Vec::with_capacity(total_files); + let mut target_dirs = HashSet::new(); + #[cfg(unix)] + let mut files_by_unix_mode = Vec::with_capacity(total_files); for i in 0..total_files { futures::executor::block_on(async { @@ -143,7 +151,7 @@ fn zip_extract>( .map_err(|s| io::Error::other(OperationError::from_state(s, &controller))) })?; - controller.set_progress((i as f32) / total_files as f32); + controller.set_progress(i as f32 / total_files as f32); let mut file = match password { None => archive.by_index(i), @@ -156,26 +164,22 @@ fn zip_extract>( let outpath = directory.as_ref().join(filepath); if file.is_dir() { - pending_directory_creates.push_back(outpath.clone()); + make_writable_dir_all(&outpath, &mut target_dirs)?; + + #[cfg(unix)] + if let Some(mode) = file.unix_mode() { + files_by_unix_mode.push((outpath, mode)); + } continue; } - let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { + + if let Some(parent) = outpath.parent() { + make_writable_dir_all(parent, &mut target_dirs)?; + } + + if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { let mut target = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut target)?; - Some(target) - } else { - None - }; - drop(file); - if let Some(target) = symlink_target { - // create all pending dirs - while let Some(pending_dir) = pending_directory_creates.pop_front() { - make_writable_dir_all(pending_dir)?; - } - - if let Some(p) = outpath.parent() { - make_writable_dir_all(p)?; - } #[cfg(unix)] { @@ -205,21 +209,10 @@ fn zip_extract>( std::os::windows::fs::symlink_file(target_path, outpath.as_path())?; } } + + written_files.push(outpath); continue; } - let mut file = match password { - None => archive.by_index(i), - Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), - }?; - - // create all pending dirs - while let Some(pending_dir) = pending_directory_creates.pop_front() { - make_writable_dir_all(pending_dir)?; - } - - if let Some(p) = outpath.parent() { - make_writable_dir_all(p)?; - } let total = file.size(); let mut outfile = fs::File::create(&outpath)?; @@ -245,13 +238,14 @@ fn zip_extract>( controller.set_progress(total_progress); } } + + // Check for real permissions, which we'll set in a second pass #[cfg(unix)] - { - // Check for real permissions, which we'll set in a second pass - if let Some(mode) = file.unix_mode() { - files_by_unix_mode.push((outpath.clone(), mode)); - } + if let Some(mode) = file.unix_mode() { + files_by_unix_mode.push((outpath.clone(), mode)); } + + written_files.push(outpath); } #[cfg(unix)] { @@ -260,11 +254,15 @@ fn zip_extract>( if files_by_unix_mode.len() > 1 { // Ensure we update children's permissions before making a parent unwritable - files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone())); + files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.components().count())); } for (path, mode) in files_by_unix_mode { fs::set_permissions(&path, fs::Permissions::from_mode(mode))?; } } + + // Flush files to disk + futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await }); + Ok(()) } diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 1442b74..b05488b 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -196,6 +196,29 @@ async fn copy_or_move( .map_err(wrap_compio_spawn_error)? } +pub async fn sync_to_disk( + written_files: Vec, + target_dirs: std::collections::HashSet, +) { + use futures::{StreamExt, stream}; + + // Sync files to disk + let file_stream = stream::iter(written_files.into_iter().map(|path| async move { + if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await { + let _ = file.sync_all().await; + } + })); + file_stream.buffer_unordered(32).collect::>().await; + + // Sync directories to disk + let dir_stream = stream::iter(target_dirs.into_iter().map(|path| async move { + if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await { + let _ = dir.sync_all().await; + } + })); + dir_stream.buffer_unordered(16).collect::>().await; +} + fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { // List of compound extensions to check const COMPOUND_EXTENSIONS: &[&str] = &[ diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index b4825e9..cc74a43 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -1,14 +1,13 @@ use compio::BufResult; use compio::buf::{IntoInner, IoBuf}; use compio::io::{AsyncReadAt, AsyncWriteAt}; -use futures::{StreamExt, stream}; use std::future::Future; use std::pin::Pin; use std::time::Instant; use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc}; use walkdir::WalkDir; -use crate::operation::OperationError; +use crate::operation::{OperationError, sync_to_disk}; use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path}; @@ -203,21 +202,8 @@ impl Context { } } - // Sync files to disk - let file_stream = stream::iter(written_files.into_iter().map(|path| async move { - if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await { - let _ = file.sync_all().await; - } - })); - file_stream.buffer_unordered(32).collect::>().await; - - // Sync directories to disk - let dir_stream = stream::iter(target_dirs.into_iter().map(|path| async move { - if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await { - let _ = dir.sync_all().await; - } - })); - dir_stream.buffer_unordered(16).collect::>().await; + // Flush files to disk + sync_to_disk(written_files, target_dirs).await; Ok(true) }