Merge branch 'master' into epoch-update

This commit is contained in:
Jeremy Soller 2026-01-26 19:18:50 -07:00 committed by GitHub
commit 00c7652cab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 220 additions and 77 deletions

View file

@ -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

View file

@ -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 }개 항목 선택됨

0
i18n/pa/cosmic_files.ftl Normal file
View file

0
i18n/ti/cosmic_files.ftl Normal file
View file

View file

@ -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<R: io::Read + io::Seek, P: AsRef<Path>>(
use std::{ffi::OsString, fs};
use zip::result::ZipError;
fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
fs::create_dir_all(outpath.as_ref())?;
fn make_writable_dir_all<T: AsRef<Path>>(
outpath: T,
target_dirs: &mut HashSet<PathBuf>,
) -> 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<R: io::Read + io::Seek, P: AsRef<Path>>(
.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<R: io::Read + io::Seek, P: AsRef<Path>>(
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<R: io::Read + io::Seek, P: AsRef<Path>>(
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<R: io::Read + io::Seek, P: AsRef<Path>>(
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<R: io::Read + io::Seek, P: AsRef<Path>>(
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(())
}

View file

@ -196,6 +196,29 @@ async fn copy_or_move(
.map_err(wrap_compio_spawn_error)?
}
pub async fn sync_to_disk(
written_files: Vec<PathBuf>,
target_dirs: std::collections::HashSet<PathBuf>,
) {
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::<Vec<_>>().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::<Vec<_>>().await;
}
fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
// List of compound extensions to check
const COMPOUND_EXTENSIONS: &[&str] = &[

View file

@ -7,7 +7,7 @@ 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};
@ -57,6 +57,8 @@ impl Context {
) -> Result<bool, OperationError> {
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 +143,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 +182,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 +202,9 @@ impl Context {
}
}
// Flush files to disk
sync_to_disk(written_files, target_dirs).await;
Ok(true)
}
@ -411,8 +428,6 @@ impl Op {
}
}
}
to_file.sync_all().await?;
}
OpKind::Move { cross_device_copy } => {
// Remove `to` if overwriting and it is an existing file