Merge pull request #1181 from storytold/fix/1163-file-list
#1163: file.list lists a folder for agents, held to the read root
This commit is contained in:
commit
7c7a7dcdc9
File diff suppressed because one or more lines are too long
|
|
@ -87,6 +87,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
always,
|
||||
load::open
|
||||
),
|
||||
cmd!(
|
||||
query "file.list",
|
||||
"List Folder",
|
||||
[],
|
||||
None,
|
||||
"{path} the files and folders in folder `path`, by name: what an agent may open there (held to the automation read root when one is in force) → {path, entries: [{name, kind: file|folder|link, size? (files, bytes)}], truncated (more than 5000 entries)} (#1163)",
|
||||
always,
|
||||
file_list
|
||||
),
|
||||
cmd!(
|
||||
query "document.serialize",
|
||||
"Serialize Document",
|
||||
|
|
@ -783,6 +792,48 @@ pub(crate) fn write_new_file(path: &str, bytes: &[u8]) -> Result<()> {
|
|||
})
|
||||
}
|
||||
|
||||
/// The most entries `file.list` returns.
|
||||
const LIST_MAX: usize = 5000;
|
||||
|
||||
fn file_list(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = p.get("path").and_then(Value::as_str).filter(|s| !s.is_empty()).ok_or_else(|| bad("file.list", "missing `path`"))?;
|
||||
let (entries, truncated) = list_dir(path, LIST_MAX)?;
|
||||
Ok(json!({ "path": absolute_path(path), "entries": entries, "truncated": truncated }))
|
||||
}
|
||||
|
||||
/// The entries of folder `path`, sorted by name: at most `max`, and whether there were more. A
|
||||
/// link isn't followed: its target may lie outside the automation roots.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn list_dir(path: &str, max: usize) -> Result<(Vec<Value>, bool)> {
|
||||
may_read(path)?;
|
||||
let dir = std::fs::read_dir(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let mut entries: Vec<(String, Value)> = vec![];
|
||||
let mut truncated = false;
|
||||
for e in dir.flatten() {
|
||||
if entries.len() == max {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let Ok(t) = e.file_type() else { continue };
|
||||
let entry = if t.is_symlink() {
|
||||
json!({ "name": name, "kind": "link" })
|
||||
} else if t.is_dir() {
|
||||
json!({ "name": name, "kind": "folder" })
|
||||
} else {
|
||||
json!({ "name": name, "kind": "file", "size": e.metadata().map(|m| m.len()).ok() })
|
||||
};
|
||||
entries.push((name, entry));
|
||||
}
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok((entries.into_iter().map(|(_, v)| v).collect(), truncated))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn list_dir(_: &str, _: usize) -> Result<(Vec<Value>, bool)> {
|
||||
Err(EngineError::Other("no file system here".into()))
|
||||
}
|
||||
|
||||
/// A file's size (bytes) and modification time (ms since the Unix epoch, when the file system
|
||||
/// keeps one); `None` when there is no file at `path` (or automation may not read it).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
|
|||
|
|
@ -1670,3 +1670,32 @@ fn path_commands_reject_bad_anchors_before_modifying_geometry() {
|
|||
s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": [{"x": 0, "y": 0}, {"x": 30, "y": 40}], "closed": false}]})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchor_count(), 2);
|
||||
}
|
||||
|
||||
/// #1163: `file.list` lists a folder's files and folders by name, held to the automation read root
|
||||
/// when one is in force.
|
||||
#[test]
|
||||
fn file_list_lists_a_folder_within_the_read_root() {
|
||||
let base = std::env::temp_dir().join(format!("vc-file-list-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
for d in ["in/sub", "outside"] {
|
||||
std::fs::create_dir_all(base.join(d)).unwrap();
|
||||
}
|
||||
std::fs::write(base.join("in/b.svg"), "<svg/>").unwrap();
|
||||
std::fs::write(base.join("in/a.pdf"), "1234").unwrap();
|
||||
let path = |d: &str| base.join(d).to_string_lossy().into_owned();
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("file.list", &json!({"path": path("in")})).unwrap();
|
||||
assert_eq!(
|
||||
r["entries"],
|
||||
json!([{"name": "a.pdf", "kind": "file", "size": 4}, {"name": "b.svg", "kind": "file", "size": 6}, {"name": "sub", "kind": "folder"}])
|
||||
);
|
||||
assert_eq!(r["truncated"], false);
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&base.join("in")), None).unwrap().unwrap();
|
||||
crate::file_access::confine(Some(&roots), || {
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/sub")})).is_ok());
|
||||
assert!(s.execute("file.list", &json!({"path": path("outside")})).is_err(), "outside the read root");
|
||||
});
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/b.svg")})).is_err(), "not a folder");
|
||||
assert!(s.execute("file.list", &json!({})).is_err());
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2101,6 +2101,7 @@
|
|||
Redefine Symbol Symbol neu definieren
|
||||
Redefine {kind} Style {kind}format neu definieren
|
||||
Redo Wiederholen
|
||||
List Folder Ordner auflisten
|
||||
Begin Undo Group Rückgängig-Gruppe beginnen
|
||||
End Undo Group Rückgängig-Gruppe beenden
|
||||
Redo {name} Wiederholen: {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2152,6 +2152,7 @@
|
|||
Redefine Symbol Redefinir símbolo
|
||||
Redefine {kind} Style Redefinir estilo de {kind}
|
||||
Redo Rehacer
|
||||
List Folder Listar carpeta
|
||||
Begin Undo Group Iniciar grupo de deshacer
|
||||
End Undo Group Finalizar grupo de deshacer
|
||||
Redo {name} Rehacer {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2149,6 +2149,7 @@
|
|||
Redefine Symbol Redéfinir le symbole
|
||||
Redefine {kind} Style Redéfinir le style de {kind}
|
||||
Redo Rétablir
|
||||
List Folder Lister le dossier
|
||||
Begin Undo Group Commencer un groupe d’annulation
|
||||
End Undo Group Terminer le groupe d’annulation
|
||||
Redo {name} Rétablir {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2152,6 +2152,7 @@
|
|||
Redefine Symbol Ridefinisci simbolo
|
||||
Redefine {kind} Style Ridefinisci stile {kind}
|
||||
Redo Ripeti
|
||||
List Folder Elenca cartella
|
||||
Begin Undo Group Inizia gruppo di annullamento
|
||||
End Undo Group Termina gruppo di annullamento
|
||||
Redo {name} Ripeti {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -101,6 +101,7 @@
|
|||
Edit 編集
|
||||
Undo 取り消し
|
||||
Redo やり直し
|
||||
List Folder フォルダーの内容を一覧表示
|
||||
Begin Undo Group 取り消しグループを開始
|
||||
End Undo Group 取り消しグループを終了
|
||||
Cut 切り取り
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2148,6 +2148,7 @@
|
|||
Redefine Symbol Переопределить символ
|
||||
Redefine {kind} Style Переопределить стиль {kind}
|
||||
Redo Повторить
|
||||
List Folder Показать содержимое папки
|
||||
Begin Undo Group Начать группу отмены
|
||||
End Undo Group Завершить группу отмены
|
||||
Redo {name} Повторить {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2144,6 +2144,7 @@
|
|||
Redefine Symbol Перевизначити символ
|
||||
Redefine {kind} Style Перевизначити стиль {kind}
|
||||
Redo Повторити
|
||||
List Folder Показати вміст теки
|
||||
Begin Undo Group Почати групу скасування
|
||||
End Undo Group Завершити групу скасування
|
||||
Redo {name} Повторити {name}
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -2077,6 +2077,7 @@
|
|||
Redefine Symbol 重新定义符号
|
||||
Redefine {kind} Style 重新定义{kind}样式
|
||||
Redo 重做
|
||||
List Folder 列出文件夹
|
||||
Begin Undo Group 开始还原组
|
||||
End Undo Group 结束还原组
|
||||
Redo {name} 重做 {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2130,6 +2130,7 @@
|
|||
Redefine Symbol 重新定義符號
|
||||
Redefine {kind} Style 重新定義{kind}樣式
|
||||
Redo 重做
|
||||
List Folder 列出資料夾
|
||||
Begin Undo Group 開始還原群組
|
||||
End Undo Group 結束還原群組
|
||||
Redo {name} 重做 {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -69,6 +69,7 @@ the suite) keep the agent to the folders you give it:
|
|||
- Every file a command writes must lie inside the **write root**: `save_file`, `export`, the save and
|
||||
export commands, Export for Screens, Package, Export for Web, print to file, `screenshot {path}`,
|
||||
library and preset saves to a path.
|
||||
- `file.list {path}` (through `run_command`) lists a folder's files and folders, held to the read root like any read, so an agent can see what it was given (#1163).
|
||||
- Read and write access are separate. Give the same folder twice for both; a root left out grants none
|
||||
of its access (with only `--automation-read-root`, nothing can be written to disk, though `export` and
|
||||
the save commands without a path still return the bytes). Without either flag nothing changes.
|
||||
|
|
|
|||
Loading…
Reference in New Issue