add rw_edit (closes #1276);

previously, only .md files were editable with permissions read+write;
all other file-extensions required the delete-permission in addition

rw_edit is the list of file-extensions to allow read+write editing of
This commit is contained in:
ed
2026-02-07 23:55:23 +00:00
parent f02e9cf6d3
commit 312f48e17c
28 changed files with 48 additions and 32 deletions

View File

@@ -1817,6 +1817,7 @@ def add_db_metadata(ap):
def add_txt(ap):
ap2 = ap.add_argument_group("textfile options")
ap2.add_argument("--rw-edit", metavar="T,T", type=u, default="md", help="comma-sep. list of file-extensions to allow editing with permissions read+write; all others require read+write+delete (volflag=rw_edit)")
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")

View File

@@ -3177,6 +3177,7 @@ class AuthSrv(object):
"unlist": vf.get("unlist") or "",
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
"rw_edit": vf["rw_edit"],
}
if "ufavico_h" in vf:
vn.js_ls["ufavico"] = vf["ufavico_h"]
@@ -3202,6 +3203,7 @@ class AuthSrv(object):
"sb_md": vn.js_ls["sb_md"],
"sba_md": vf.get("md_sba") or "",
"sba_lg": vf.get("lg_sba") or "",
"rw_edit": vf["rw_edit"],
"txt_ext": self.args.textfiles.replace(",", " "),
"def_hcols": list(vf.get("mth") or []),
"unlist0": vf.get("unlist") or "",

View File

@@ -139,6 +139,7 @@ def vf_vmap() -> dict[str, str]:
"rss_sort",
"rss_fmt_t",
"rss_fmt_d",
"rw_edit",
"shr_who",
"sort",
"tail_fd",
@@ -389,6 +390,7 @@ flagcats = {
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
},
"textfiles": {
"rw_edit=md,txt": "only require read+write to edit .md and .txt",
"md_no_br": "newline only on double-newline or two tailing spaces",
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"exp": "enable textfile expansion; see --help-exp",

View File

@@ -3532,9 +3532,12 @@ class HttpCli(object):
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
self._assert_safe_rem(rem)
if not self.can_delete and not new_file.lower().endswith(".md"):
t = "you can only create .md files because you don't have the delete-permission"
raise Pebkac(400, t)
if not self.can_delete and (
"." not in new_file
or new_file.rsplit(".", 1)[1].lower() not in self.args.rw_edit_set
):
t = "you can only create %s files because you don't have the delete-permission"
raise Pebkac(400, t % (self.args.rw_edit.replace(",", "/")))
sanitized = sanitize_fn(new_file)
fdir = vfs.canonical(rem)
@@ -4051,8 +4054,11 @@ class HttpCli(object):
rem = "{}/{}".format(rp, fn).strip("/")
dbv, vrem = vfs.get_dbv(rem)
if not rem.lower().endswith(".md") and not self.can_delete:
raise Pebkac(400, "only markdown pls")
if not self.can_delete and (
"." not in rem or rem.rsplit(".", 1)[1].lower() not in self.args.rw_edit_set
):
t = "you can only edit %s files because you don't have the delete-permission"
raise Pebkac(400, t % (self.args.rw_edit.replace(",", "/")))
if nullwrite:
response = json.dumps({"ok": True, "lastmod": 0})

View File

@@ -1116,7 +1116,7 @@ class SvcHub(object):
vs = os.path.expandvars(os.path.expanduser(vs))
setattr(al, k, vs)
for k in "idp_adm stats_u".split(" "):
for k in "idp_adm rw_edit stats_u".split(" "):
vs = getattr(al, k)
vsa = [x.strip() for x in vs.split(",")]
vsa = [x.lower() for x in vsa if x]

View File

@@ -2,7 +2,8 @@
var J_BRW = 1;
if (!window.drcm) alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser');
if (window.rw_edit === undefined)
alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser');
var XHR = XMLHttpRequest,
img_re = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
@@ -462,7 +463,7 @@ if (1)
"mk_noname": "type a name into the text field on the left before you do that :p",
"nmd_i1": "also add the file extension you want, for example <code>.md</code>",
"nmd_i2": "you can only create <code>.md</code> files because you don't have the delete-permission",
"nmd_i2": "you can only create <code>.{0}</code> files because you don't have the delete-permission",
"tv_load": "Loading text document:\n\n{0}\n\n{1}% ({2} of {3} MiB loaded)",
"tv_xe1": "could not load textfile:\n\nerror ",
@@ -7589,7 +7590,7 @@ var treectl = (function () {
reload_tree();
reload_browser();
tree_scrollto();
if (res.acct) {
if (res.cfg) {
acct = res.acct;
have_up2k_idx = res.idx;
have_tags_idx = res.itag;
@@ -7978,7 +7979,10 @@ function apply_perms(res) {
if (up2k)
up2k.set_fsearch();
ebi('new_mdi').innerHTML = has(perms, "delete") ? L.nmd_i1 : L.nmd_i2;
if (res.cfg)
rw_edit = res.rw_edit;
window.re_rw_edit = new RegExp('\.(' + rw_edit.replace(/,/g, '|') + ')$', 'i');
ebi('new_mdi').innerHTML = has(perms, "delete") ? L.nmd_i1 : L.nmd_i2.format(rw_edit.replace(/,/g, '/'));
widget.setvis();
thegrid.setvis();
@@ -8784,7 +8788,7 @@ var msel = (function () {
tb = QS('#op_new_md input[name="name"]');
form.onsubmit = function (e) {
if (!has(perms, "delete") && !/\.md$/.test(tb.value)) {
if (!has(perms, "delete") && !re_rw_edit.test(tb.value)) {
ev(e);
toast.err(10, L.nmd_i2);
return false;

View File

@@ -448,7 +448,7 @@ Ls.chi = {
"mk_noname": "在左侧文本框中输入名称,然后再执行此操作 :p",
"nmd_i1": "还可以添加需要的文件扩展名,例如 <code>.md</code>", //m
"nmd_i2": "由于没有删除权限,你只能创建 <code>.md</code> 文件", //m
"nmd_i2": "由于没有删除权限,你只能创建 <code>.{0}</code> 文件", //m
"tv_load": "加载文本文件:\n\n{0}\n\n{1}% ({2} 的 {3} MiB 已加载)",
"tv_xe1": "无法加载文本文件:\n\n错误 ",

View File

@@ -452,7 +452,7 @@ Ls.cze = {
"mk_noname": "napište název do textového pole vlevo předtím než to uděláte :p",
"nmd_i1": "můžeš také přidat příponu souboru, například <code>.md</code>", //m
"nmd_i2": "můžeš vytvářet pouze <code>.md</code> soubory, protože nemáš oprávnění mazat", //m
"nmd_i2": "můžeš vytvářet pouze <code>.{0}</code> soubory, protože nemáš oprávnění mazat", //m
"tv_load": "Načítání textového dokumentu:\n\n{0}\n\n{1}% ({2} z {3} MiB načteno)",
"tv_xe1": "nelze načíst textový soubor:\n\nchyba ",

View File

@@ -448,7 +448,7 @@ Ls.deu = {
"mk_noname": "Tipp' mal vorher lieber einen Namen in das Textfeld links, bevor du das machst :p",
"nmd_i1": "Füge auch die Dateiendung hinzu, z.B. <code>.md</code>",
"nmd_i2": "Du kannst nur <code>.md</code>-Dateien erstellen, da dir Lösch-Rechte fehlen",
"nmd_i2": "Du kannst nur <code>.{0}</code>-Dateien erstellen, da dir Lösch-Rechte fehlen",
"tv_load": "Textdatei wird geladen:\n\n{0}\n\n{1}% ({2} von {3} MiB geladen)",
"tv_xe1": "Konnte Textdatei nicht laden:\n\nFehler ",

View File

@@ -448,7 +448,7 @@ Ls.epo = {
"mk_noname": "tajpu nomon en tekstokampo maldekstre antaŭ vi faras ĉi tion :p",
"nmd_i1": "vi povas aldoni la deziratan sufikson, ekzemple <code>.md</code>", //m
"nmd_i2": "vi povas krei nur <code>.md</code>-dosierojn ĉar vi ne havas forigan permeson", //m
"nmd_i2": "vi povas krei nur <code>.{0}</code>-dosierojn ĉar vi ne havas forigan permeson", //m
"tv_load": "Ŝargado de teksto-dokumento:\n\n{0}\n\n{1}% ({2} da {3} MiB ŝargita)",
"tv_xe1": "ne povas ŝargi teksto-dosieron:\n\neraro ",

View File

@@ -448,7 +448,7 @@ Ls.fin = {
"mk_noname": "kirjoita nimi vasemmalla olevaan tekstikenttään ennen kuin teet tuon :p",
"nmd_i1": "voit myös lisätä haluamasi tiedostopäätteen, esimerkiksi <code>.md</code>", //m
"nmd_i2": "voit luoda vain <code>.md</code>-tiedostoja, koska sinulla ei ole poistolupaa", //m
"nmd_i2": "voit luoda vain <code>.{0}</code>-tiedostoja, koska sinulla ei ole poistolupaa", //m
"tv_load": "Ladataan tekstidokumenttia:\n\n{0}\n\n{1}% ({2} / {3} Mt ladattu)",
"tv_xe1": "tekstitiedoston lataaminen epäonnistui:\n\nvirhe ",

View File

@@ -448,7 +448,7 @@ Ls.fra = {
"mk_noname": "entrez un nom dans le champ de texte à gauche avant de faire ça :p",
"nmd_i1": "ajoutez aussi lextension souhaitée, par exemple <code>.md</code>", //m
"nmd_i2": "vous ne pouvez créer que des fichiers <code>.md</code> car vous navez pas la permission deffacer", //m
"nmd_i2": "vous ne pouvez créer que des fichiers <code>.{0}</code> car vous navez pas la permission deffacer", //m
"tv_load": "Chargement du document texte:\n\n{0}\n\n{1}% ({2} de {3} MiB chargés)",
"tv_xe1": "impossible de charger le fichier texte:\n\nerreur",

View File

@@ -448,7 +448,7 @@ Ls.grc = {
"mk_noname": "γράψε ένα όνομα στο πεδίο κειμένου αριστερά πριν το κάνεις :p",
"nmd_i1": "μπορείτε επίσης να προσθέσετε την κατάληξη που θέλετε, όπως <code>.md</code>", //m
"nmd_i2": "μπορείτε να δημιουργήσετε μόνο αρχεία <code>.md</code> επειδή δεν έχετε δικαίωμα διαγραφής", //m
"nmd_i2": "μπορείτε να δημιουργήσετε μόνο αρχεία <code>.{0}</code> επειδή δεν έχετε δικαίωμα διαγραφής", //m
"tv_load": "Φόρτωση αρχείου κειμένου:\n\n{0}\n\n{1}% ({2} από {3} MiB φορτωμένα)",
"tv_xe1": "αδυναμία φόρτωσης αρχείου κειμένου:\n\nσφάλμα ",

View File

@@ -448,7 +448,7 @@ Ls.ita = {
"mk_noname": "scrivi un nome nel campo di testo a sinistra prima di farlo :p",
"nmd_i1": "puoi anche aggiungere lestensione che vuoi, per esempio <code>.md</code>", //m
"nmd_i2": "puoi creare solo file <code>.md</code> perché non hai il permesso di eliminare", //m
"nmd_i2": "puoi creare solo file <code>.{0}</code> perché non hai il permesso di eliminare", //m
"tv_load": "Caricando documento di testo:\n\n{0}\n\n{1}% ({2} di {3} MiB caricati)",
"tv_xe1": "impossibile caricare file di testo:\n\nerrore ",

View File

@@ -448,7 +448,7 @@ Ls.jpn = {
"mk_noname": "それをする前に左側のテキストフィールドに名前を入力してください :p",
"nmd_i1": "必要なファイル拡張子も追加します。例: <code>.md</code>",
"nmd_i2": "削除権限がないため、<code>.md</code> ファイルのみを作成できます",
"nmd_i2": "削除権限がないため、<code>.{0}</code> ファイルのみを作成できます",
"tv_load": "テキストドキュメントの読み込み中:\n\n{0}\n\n{1}%{2} / {3} MiB ロード済み)",
"tv_xe1": "テキストファイルを読み込めませんでした:\n\nエラー ",

View File

@@ -448,7 +448,7 @@ Ls.kor = {
"mk_noname": "왼쪽 텍스트 필드에 이름을 먼저 입력해주세요 :p",
"nmd_i1": "원하는 파일 확장자를 추가할 수 있습니다. 예: <code>.md</code>", //m
"nmd_i2": "삭제 권한이 없어서 <code>.md</code> 파일만 만들 수 있습니다", //m
"nmd_i2": "삭제 권한이 없어서 <code>.{0}</code> 파일만 만들 수 있습니다", //m
"tv_load": "텍스트 문서 불러오는 중:\n\n{0}\n\n{1}% ({3} MiB 중 {2} MiB 로드됨)",
"tv_xe1": "텍스트 파일을 불러올 수 없습니다:\n\n오류 ",

View File

@@ -448,7 +448,7 @@ Ls.nld = {
"mk_noname": "Voer een naam in het tekstveld aan de linkerkant voordat je verder gaat :p",
"nmd_i1": "Voeg ook de gewenste extensie toe, bijvoorbeeld <code>.md</code>", //m
"nmd_i2": "Je kunt alleen <code>.md</code>-bestanden maken omdat je geen verwijderrechten hebt", //m
"nmd_i2": "Je kunt alleen <code>.{0}</code>-bestanden maken omdat je geen verwijderrechten hebt", //m
"tv_load": "Tekstdocument laden:\n\n{0}\n\n{1}% ({2} van de {3} MiB geladen)",
"tv_xe1": "Kon tekstbestand niet laden:\n\nfout ",

View File

@@ -445,7 +445,7 @@ Ls.nno = {
"mk_noname": "skriv inn eit namn i tekstboksa åt venstre først :p",
"nmd_i1": "leggja også til filendinga du vil, til dømes <code>.md</code>", //m
"nmd_i2": "du kan berre laga <code>.md</code>-filer fordi du ikkje har delete-tilgang", //m
"nmd_i2": "du kan berre laga <code>.{0}</code>-filer fordi du ikkje har delete-tilgang", //m
"tv_load": "Lastar inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lasta ned)",
"tv_xe1": "kunne ikkje laste tekstfil:\n\nfeil ",

View File

@@ -445,7 +445,7 @@ Ls.nor = {
"mk_noname": "skriv inn et navn i tekstboksen til venstre først :p",
"nmd_i1": "legg også til ønsket filtype, for eksempel <code>.md</code>", //m
"nmd_i2": "du kan bare lage <code>.md</code>-filer fordi du ikke har delete-tilgang", //m
"nmd_i2": "du kan bare lage <code>.{0}</code>-filer fordi du ikke har delete-tilgang", //m
"tv_load": "Laster inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lastet ned)",
"tv_xe1": "kunne ikke laste tekstfil:\n\nfeil ",

View File

@@ -451,7 +451,7 @@ Ls.pol = {
"mk_noname": "wpisz nazwę do pola po lewej zanim to zrobisz :p",
"nmd_i1": "możesz też dodać wybrane rozszerzenie, np. <code>.md</code>", //m
"nmd_i2": "możesz tworzyć tylko pliki <code>.md</code>, ponieważ nie masz uprawnień do usuwania", //m
"nmd_i2": "możesz tworzyć tylko pliki <code>.{0}</code>, ponieważ nie masz uprawnień do usuwania", //m
"tv_load": "Wczytywanie pliku tekstowego:\n\n{0}\n\n{1}% (wczytano {2} z {3} MiB)",
"tv_xe1": "nie udało się wczytać pliku:\n\nbłąd ",

View File

@@ -448,7 +448,7 @@ Ls.por = {
"mk_noname": "digite um nome no campo de texto à esquerda antes de fazer isso :p",
"nmd_i1": "também adicione a extensão desejada, por exemplo <code>.md</code>",
"nmd_i2": "só pode criar arquivos <code>.md</code> porque não tem permissão para apagar",
"nmd_i2": "só pode criar arquivos <code>.{0}</code> porque não tem permissão para apagar",
"tv_load": "Carregando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB carregados)",
"tv_xe1": "não foi possível carregar o arquivo de texto:\n\nerro ",

View File

@@ -448,7 +448,7 @@ Ls.rus = {
"mk_noname": "введите имя в текстовое поле слева перед тем, как это делать :p",
"nmd_i1": "вы также можете указать нужное расширение, например <code>.md</code>", //m
"nmd_i2": "вы можете создавать только файлы <code>.md</code>, так как у вас нет разрешения на удаление", //m
"nmd_i2": "вы можете создавать только файлы <code>.{0}</code>, так как у вас нет разрешения на удаление", //m
"tv_load": "Загружаю текстовый документ:\n\n{0}\n\n{1}% ({2} из {3} МиБ загружено)",
"tv_xe1": "не удалось загрузить текстовый файл:\n\nошибка ",

View File

@@ -447,7 +447,7 @@ Ls.spa = {
"mk_noname": "escribe un nombre en el campo de texto de la izquierda antes de hacer eso :p",
"nmd_i1": "también puedes añadir la extensión que quieras, por ejemplo <code>.md</code>", //m
"nmd_i2": "solo puedes crear archivos <code>.md</code> porque no tienes permiso para borrar", //m
"nmd_i2": "solo puedes crear archivos <code>.{0}</code> porque no tienes permiso para borrar", //m
"tv_load": "Cargando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB cargados)",
"tv_xe1": "no se pudo cargar el archivo de texto:\n\nerror ",

View File

@@ -448,7 +448,7 @@ Ls.swe = {
"mk_noname": "skriv ett namn i fältet till vänster först :p",
"nmd_i1": "lägg också till filändelsen du vill ha, till exempel <code>.md</code>", //m
"nmd_i2": "du kan bara skapa <code>.md</code>-filer eftersom du inte har borttagningsbehörighet", //m
"nmd_i2": "du kan bara skapa <code>.{0}</code>-filer eftersom du inte har borttagningsbehörighet", //m
"tv_load": "Laddar textfil:\n\n{0}\n\n{1}% ({2} av {3} MiB laddat)",
"tv_xe1": "kunde ej ladda textfil:\n\nfel ",

View File

@@ -448,7 +448,7 @@ Ls.tur = {
"mk_noname": "bunu yapmadan önce soldaki boşluğa bir şeyler yazsana :p",
"nmd_i1": "ayrıca istediğin dosya uzantısını ekleyebilirsin, örneğin <code>.md</code>", //m
"nmd_i2": "silme iznin olmadığı için yalnızca <code>.md</code> dosyaları oluşturabilirsin", //m
"nmd_i2": "silme iznin olmadığı için yalnızca <code>.{0}</code> dosyaları oluşturabilirsin", //m
"tv_load": "Metin belgesi yükleniyor:\n\n{0}\n\n{1}% ({2} of {3} MiB yüklendi)",
"tv_xe1": "metin dosyası yüklenemedi:\n\nhata ",

View File

@@ -448,7 +448,7 @@ Ls.ukr = {
"mk_noname": "введіть ім'я в текстове поле зліва перед тим, як робити це :p",
"nmd_i1": "ви також можете додати потрібне розширення, наприклад <code>.md</code>", //m
"nmd_i2": "ви можете створювати тільки файли <code>.md</code>, оскільки не маєте дозволу на видалення", //m
"nmd_i2": "ви можете створювати тільки файли <code>.{0}</code>, оскільки не маєте дозволу на видалення", //m
"tv_load": "Завантаження текстового документа:\n\n{0}\n\n{1}% ({2} з {3} MiB завантажено)",
"tv_xe1": "не вдалося завантажити текстовий файл:\n\nпомилка ",

View File

@@ -457,7 +457,7 @@ Ls.vie = {
"mk_noname": "hãy nhập tên vào ô bên trái trước khi thực hiện :p",
"nmd_i1": "hãy thêm cả phần mở rộng tệp bạn muốn, ví dụ <code>.md</code>",
"nmd_i2": "bạn chỉ có thể tạo tệp <code>.md</code> vì bạn không có quyền xóa",
"nmd_i2": "bạn chỉ có thể tạo tệp <code>.{0}</code> vì bạn không có quyền xóa",
"tv_load": "Đang tải tài liệu văn bản:\n\n{0}\n\n{1}% ({2} / {3} MiB)",
"tv_xe1": "không thể tải tệp văn bản:\n\nlỗi ",

View File

@@ -212,6 +212,7 @@ class Cfg(Namespace):
rm_retry="0/0",
rotf_tz="UTC",
rss_sort="m",
rw_edit="md",
s_rd_sz=256 * 1024,
s_wr_sz=256 * 1024,
shr_who="auth",