feat(encryption): add UI and settings for file encryption

This commit is contained in:
Aaron Liu 2025-10-24 15:04:51 +08:00
parent 1c9dd8d9ad
commit 8b91fca929
47 changed files with 1313 additions and 670 deletions

View File

@ -436,7 +436,12 @@
"viewSetting": "Ansichtseinstellungen",
"saved": "Gespeichert",
"notSet": "Nicht gesetzt",
"deleteViewSetting": "Ansichtseinstellung löschen"
"deleteViewSetting": "Ansichtseinstellung löschen",
"encryption": "Verschlüsselung",
"fullEncryption": "Mit {{cipher}} verschlüsselt",
"partialEncryption": "Teilweise verschlüsselt",
"partialEncryptionDes": "Einige Versionen oder andere Daten dieser Datei sind nicht verschlüsselt.",
"noEncryption": "Nicht verschlüsselt"
},
"modals": {
"includePasswordInShareLink": "Passwort in Link einbeziehen",

View File

@ -717,7 +717,15 @@
"blobUrlCache": "Blob-URL-Cache",
"clearBlobUrlCache": "Blob-URL-Cache löschen",
"clearBlobUrlCacheDes": "Um die Cache-Trefferrate zu erhöhen, speichert und wiederverwendet Cloudreve Blob-URLs zwischen. Wenn sich die CDN-Adresse oder andere Einstellungen ändern, löschen Sie bitte den Cache.",
"cacheCleared": "Cache gelöscht."
"cacheCleared": "Cache gelöscht.",
"masterEncryptionKeyVault": "Speicherung des Hauptverschlüsselungsschlüssels",
"masterEncryptionKeyVaultDes": "Wählen Sie aus, wie der Hauptverschlüsselungsschlüssel gespeichert werden soll. Wird nach Neustart wirksam. Wenn Sie die Speichermethode oder den Schlüssel ändern müssen, befolgen Sie strikt die Schritte in der <0>Dokumentation</0>, um Datenverlust zu vermeiden.",
"masterEncryptionKeyVaultSetting": "Datenbank",
"masterEncryptionKeyVaultEnv": "Umgebungsvariable <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "Datei",
"masterEncryptionKeyVaultFilePath": "Schlüsselspeicherpfad",
"showEncryptionStatus": "Verschlüsselungsstatus anzeigen",
"showEncryptionStatusDes": "Wenn aktiviert, können Benutzer den Verschlüsselungsstatus von Dateien in den Dateidetails anzeigen."
},
"giftCodes": {
"giftCodesSettings": "Geschenkcodes",
@ -1033,7 +1041,11 @@
"sharePointUrlDes": "Geben Sie die SharePoint-Site-URL ein. Nach dem Verlust des Fokus konvertiert das System sie automatisch in die korrekte Treiber-Kennung.",
"ks3selectRegionDes": "Geben Sie den Regionscode des Speicher-Buckets ein, z.B. <0>BEIJING</0>.",
"ks3EndpointPathStyle": "Wählen Sie das Format der KS3-Endpoint-Adresse.",
"ossRegionDes": "Geben Sie den Regionscode ein, in dem sich der Bucket befindet, z.B. <0>cn-hangzhou</0>. Sie können die entsprechende Region in der Tabelle <1>OSS-Regionen und Endpunkte</1> finden und die entsprechende <2>Regions-ID</2> ausfüllen."
"ossRegionDes": "Geben Sie den Regionscode ein, in dem sich der Bucket befindet, z.B. <0>cn-hangzhou</0>. Sie können die entsprechende Region in der Tabelle <1>OSS-Regionen und Endpunkte</1> finden und die entsprechende <2>Regions-ID</2> ausfüllen.",
"fileEncryption": "Dateiverschlüsselung",
"enableFileEncryption": "Dateiverschlüsselung aktivieren",
"enableFileEncryptionDes": "Wenn aktiviert, werden Datei-Blobs verschlüsselt gespeichert. Die Änderung dieser Einstellung wirkt sich nur auf neu hinzugefügte Dateien aus.",
"encryptedFile": "Verschlüsselte Dateien"
},
"node": {
"slave": "Slave",

View File

@ -401,7 +401,12 @@
"viewSetting": "View setting",
"saved": "Saved",
"notSet": "Not set",
"deleteViewSetting": "Delete view setting"
"deleteViewSetting": "Delete view setting",
"encryption": "Encryption",
"fullEncryption": "Encrypted with {{cipher}}",
"partialEncryption": "Partially encrypted",
"partialEncryptionDes": "Some versions or other data of this file are not encrypted.",
"noEncryption": "Not encrypted"
},
"modals": {
"includePasswordInShareLink": "Include password in share link",

View File

@ -716,7 +716,15 @@
"blobUrlCache": "Blob URL cache",
"clearBlobUrlCache": "Clear Blob URL cache",
"clearBlobUrlCacheDes": "To increase cache hit rate, Cloudreve caches and reuses Blob URLs. When the CDN address or other settings change, please clear the cache.",
"cacheCleared": "Cache cleared."
"cacheCleared": "Cache cleared.",
"masterEncryptionKeyVault": "Master encryption key storage",
"masterEncryptionKeyVaultDes": "Choose how to store the master encryption key, takes effect after restart. If you need to change the storage method or key, strictly follow the steps in the <0>documentation</0> to avoid data loss.",
"masterEncryptionKeyVaultSetting": "Database",
"masterEncryptionKeyVaultEnv": "Environment variable <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "File",
"masterEncryptionKeyVaultFilePath": "Key storage path",
"showEncryptionStatus": "Show encryption status",
"showEncryptionStatusDes": "When enabled, users can view the encryption status of files in file details."
},
"giftCodes": {
"giftCodesSettings": "Gift Codes",
@ -1034,7 +1042,11 @@
"sharePointUrlDes": "Enter the SharePoint site URL. After losing focus, the system will automatically convert it to the correct driver identifier.",
"ks3selectRegionDes": "Enter the region code of the storage bucket, e.g. <0>BEIJING</0> .",
"ks3EndpointPathStyle": "Select the format of the KS3 Endpoint address.",
"ossRegionDes": "Enter the region code of the storage bucket, e.g. <0>cn-hangzhou</0>. You can find the corresponding region in the <1>OSS regions and endpoints</1> table and fill in the corresponding <2>Region ID</2>."
"ossRegionDes": "Enter the region code of the storage bucket, e.g. <0>cn-hangzhou</0>. You can find the corresponding region in the <1>OSS regions and endpoints</1> table and fill in the corresponding <2>Region ID</2>.",
"fileEncryption": "File encryption",
"enableFileEncryption": "Enable file encryption",
"enableFileEncryptionDes": "When enabled, file blobs will be stored encrypted. Changing this setting only affects newly added files.",
"encryptedFile": "Encrypted files"
},
"node": {
"slave": "slave",

View File

@ -436,7 +436,12 @@
"viewSetting": "Configuración de vista",
"saved": "Guardado",
"notSet": "Sin establecer",
"deleteViewSetting": "Eliminar configuración de vista"
"deleteViewSetting": "Eliminar configuración de vista",
"encryption": "Cifrado",
"fullEncryption": "Cifrado con {{cipher}}",
"partialEncryption": "Parcialmente cifrado",
"partialEncryptionDes": "Algunas versiones u otros datos de este archivo no están cifrados.",
"noEncryption": "No cifrado"
},
"modals": {
"includePasswordInShareLink": "Incluir contraseña en enlace compartido",

View File

@ -717,7 +717,15 @@
"blobUrlCache": "Cache de URL de Blob",
"clearBlobUrlCache": "Limpiar cache de URL de Blob",
"clearBlobUrlCacheDes": "Para aumentar la tasa de aciertos de cache, Cloudreve cachea y reutiliza URLs de Blob. Cuando la dirección CDN u otras configuraciones cambien, por favor limpia el cache.",
"cacheCleared": "Cache limpiado."
"cacheCleared": "Cache limpiado.",
"masterEncryptionKeyVault": "Almacenamiento de clave de cifrado maestra",
"masterEncryptionKeyVaultDes": "Elija cómo almacenar la clave de cifrado maestra, surte efecto después de reiniciar. Si necesita cambiar el método de almacenamiento o la clave, siga estrictamente los pasos en la <0>documentación</0> para evitar la pérdida de datos.",
"masterEncryptionKeyVaultSetting": "Base de datos",
"masterEncryptionKeyVaultEnv": "Variable de entorno <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "Archivo",
"masterEncryptionKeyVaultFilePath": "Ruta de almacenamiento de clave",
"showEncryptionStatus": "Mostrar estado de cifrado",
"showEncryptionStatusDes": "Cuando está habilitado, los usuarios pueden ver el estado de cifrado de los archivos en los detalles del archivo."
},
"giftCodes": {
"giftCodesSettings": "Códigos de Regalo",
@ -1033,7 +1041,11 @@
"sharePointUrlDes": "Ingresa la URL del sitio SharePoint. Después de perder el foco, el sistema convertirá automáticamente a la identificación correcta del controlador.",
"ks3selectRegionDes": "Ingresa el código de región del bucket de almacenamiento, ej. <0>BEIJING</0> .",
"ks3EndpointPathStyle": "Selecciona el formato de la dirección del Endpoint KS3.",
"ossRegionDes": "Ingresa el código de región donde se encuentra el bucket, ej. <0>cn-hangzhou</0>. Puedes encontrar la región correspondiente en la tabla <1>Regiones y endpoints de OSS</1> y completar el <2>ID de región</2> correspondiente."
"ossRegionDes": "Ingresa el código de región donde se encuentra el bucket, ej. <0>cn-hangzhou</0>. Puedes encontrar la región correspondiente en la tabla <1>Regiones y endpoints de OSS</1> y completar el <2>ID de región</2> correspondiente.",
"fileEncryption": "Cifrado de archivos",
"enableFileEncryption": "Habilitar cifrado de archivos",
"enableFileEncryptionDes": "Cuando está habilitado, los blobs de archivos se almacenarán cifrados. Cambiar esta configuración solo afecta a los archivos recién agregados.",
"encryptedFile": "Archivos cifrados"
},
"node": {
"slave": "esclavo",

View File

@ -436,7 +436,12 @@
"viewSetting": "Paramètre de vue",
"saved": "Enregistré",
"notSet": "Non défini",
"deleteViewSetting": "Supprimer le paramètre de vue"
"deleteViewSetting": "Supprimer le paramètre de vue",
"encryption": "Chiffrement",
"fullEncryption": "Chiffré avec {{cipher}}",
"partialEncryption": "Partiellement chiffré",
"partialEncryptionDes": "Certaines versions ou autres données de ce fichier ne sont pas chiffrées.",
"noEncryption": "Non chiffré"
},
"modals": {
"includePasswordInShareLink": "Inclure le mot de passe dans le lien de partage",

View File

@ -717,7 +717,15 @@
"blobUrlCache": "Cache d'URL Blob",
"clearBlobUrlCache": "Vider le cache d'URL Blob",
"clearBlobUrlCacheDes": "Pour augmenter le taux de réussite du cache, Cloudreve met en cache et réutilise les URL Blob. Lorsque l'adresse CDN ou d'autres paramètres changent, veuillez vider le cache.",
"cacheCleared": "Cache vidé."
"cacheCleared": "Cache vidé.",
"masterEncryptionKeyVault": "Stockage de la clé de chiffrement principale",
"masterEncryptionKeyVaultDes": "Choisissez comment stocker la clé de chiffrement principale, prend effet après le redémarrage. Si vous devez changer la méthode de stockage ou la clé, suivez strictement les étapes de la <0>documentation</0> pour éviter la perte de données.",
"masterEncryptionKeyVaultSetting": "Base de données",
"masterEncryptionKeyVaultEnv": "Variable d'environnement <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "Fichier",
"masterEncryptionKeyVaultFilePath": "Chemin de stockage de la clé",
"showEncryptionStatus": "Afficher l'état du chiffrement",
"showEncryptionStatusDes": "Lorsqu'il est activé, les utilisateurs peuvent consulter l'état du chiffrement des fichiers dans les détails du fichier."
},
"giftCodes": {
"giftCodesSettings": "Codes cadeaux",
@ -1033,7 +1041,11 @@
"sharePointUrlDes": "Entrez l'URL du site SharePoint. Après avoir perdu le focus, le système convertira automatiquement vers l'identifiant de pilote correct.",
"ks3selectRegionDes": "Entrez le code de région du bucket de stockage, par ex. <0>BEIJING</0> .",
"ks3EndpointPathStyle": "Sélectionnez le format de l'adresse Endpoint KS3.",
"ossRegionDes": "Entrez le code de région où se trouve le bucket, par ex. <0>cn-hangzhou</0>. Vous pouvez trouver la région correspondante dans le tableau <1>Régions et points de terminaison OSS</1> et remplir l'<2>ID de région</2> correspondant."
"ossRegionDes": "Entrez le code de région où se trouve le bucket, par ex. <0>cn-hangzhou</0>. Vous pouvez trouver la région correspondante dans le tableau <1>Régions et points de terminaison OSS</1> et remplir l'<2>ID de région</2> correspondant.",
"fileEncryption": "Chiffrement des fichiers",
"enableFileEncryption": "Activer le chiffrement des fichiers",
"enableFileEncryptionDes": "Lorsqu'il est activé, les blobs de fichiers seront stockés chiffrés. La modification de ce paramètre n'affecte que les fichiers nouvellement ajoutés.",
"encryptedFile": "Fichiers chiffrés"
},
"node": {
"slave": "esclave",

View File

@ -436,7 +436,12 @@
"viewSetting": "Impostazioni visualizzazione",
"saved": "Salvato",
"notSet": "Non impostato",
"deleteViewSetting": "Elimina impostazioni visualizzazione"
"deleteViewSetting": "Elimina impostazioni visualizzazione",
"encryption": "Crittografia",
"fullEncryption": "Crittografato con {{cipher}}",
"partialEncryption": "Parzialmente crittografato",
"partialEncryptionDes": "Alcune versioni o altri dati di questo file non sono crittografati.",
"noEncryption": "Non crittografato"
},
"modals": {
"includePasswordInShareLink": "Includi password nel link di condivisione",

View File

@ -717,7 +717,15 @@
"blobUrlCache": "Cache URL Blob",
"clearBlobUrlCache": "Pulisci cache URL Blob",
"clearBlobUrlCacheDes": "Per aumentare il tasso di hit della cache, Cloudreve memorizza nella cache e riutilizza gli URL Blob. Quando l'indirizzo CDN o altre impostazioni cambiano, pulisci la cache.",
"cacheCleared": "Cache pulita."
"cacheCleared": "Cache pulita.",
"masterEncryptionKeyVault": "Archiviazione chiave di crittografia principale",
"masterEncryptionKeyVaultDes": "Scegli come archiviare la chiave di crittografia principale, ha effetto dopo il riavvio. Se è necessario modificare il metodo di archiviazione o la chiave, segui rigorosamente i passaggi nella <0>documentazione</0> per evitare la perdita di dati.",
"masterEncryptionKeyVaultSetting": "Database",
"masterEncryptionKeyVaultEnv": "Variabile d'ambiente <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "File",
"masterEncryptionKeyVaultFilePath": "Percorso di archiviazione della chiave",
"showEncryptionStatus": "Mostra stato crittografia",
"showEncryptionStatusDes": "Quando abilitato, gli utenti possono visualizzare lo stato di crittografia dei file nei dettagli del file."
},
"giftCodes": {
"giftCodesSettings": "Codici Regalo",
@ -1032,7 +1040,11 @@
"sharePointUrlDes": "Inserisci l'URL del sito SharePoint. Dopo aver perso il focus, il sistema lo convertirà automaticamente nell'identificatore driver corretto.",
"ks3selectRegionDes": "Inserisci il codice regione del bucket di archiviazione, ad es. <0>BEIJING</0>.",
"ks3EndpointPathStyle": "Seleziona il formato dell'indirizzo KS3 Endpoint.",
"ossRegionDes": "Inserisci il codice regione dove si trova il bucket, ad es. <0>cn-hangzhou</0>. Puoi trovare la regione corrispondente nella tabella <1>Regioni ed endpoint OSS</1> e inserire il <2>ID regione</2> corrispondente."
"ossRegionDes": "Inserisci il codice regione dove si trova il bucket, ad es. <0>cn-hangzhou</0>. Puoi trovare la regione corrispondente nella tabella <1>Regioni ed endpoint OSS</1> e inserire il <2>ID regione</2> corrispondente.",
"fileEncryption": "Crittografia file",
"enableFileEncryption": "Abilita crittografia file",
"enableFileEncryptionDes": "Quando abilitato, i blob dei file verranno archiviati crittografati. La modifica di questa impostazione influisce solo sui file appena aggiunti.",
"encryptedFile": "File crittografati"
},
"node": {
"slave": "slave",

View File

@ -401,7 +401,12 @@
"viewSetting": "ビュー設定",
"saved": "保存",
"notSet": "未設定",
"deleteViewSetting": "ビュー設定を削除"
"deleteViewSetting": "ビュー設定を削除",
"encryption": "暗号化",
"fullEncryption": "{{cipher}} で暗号化",
"partialEncryption": "部分的に暗号化",
"partialEncryptionDes": "このファイルの一部のバージョンまたは他のデータは暗号化されていません。",
"noEncryption": "暗号化されていません"
},
"modals": {
"includePasswordInShareLink": "共有リンクにパスワードを含める",

View File

@ -716,7 +716,15 @@
"blobUrlCache": "Blob URLキャッシュ",
"clearBlobUrlCache": "Blob URLキャッシュのクリア",
"clearBlobUrlCacheDes": "キャッシュヒット率を高めるため、CloudreveはBlob URLをキャッシュして再利用します。CDNアドレスなどの設定が変更された場合は、キャッシュをクリアしてください。",
"cacheCleared": "キャッシュをクリアしました"
"cacheCleared": "キャッシュをクリアしました",
"masterEncryptionKeyVault": "マスター暗号化キーの保存方法",
"masterEncryptionKeyVaultDes": "マスター暗号化キーの保存方法を選択します。再起動後に有効になります。保存方法またはキーを変更する必要がある場合は、データ損失を避けるために<0>ドキュメント</0>の手順に厳密に従ってください。",
"masterEncryptionKeyVaultSetting": "データベース",
"masterEncryptionKeyVaultEnv": "環境変数 <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "ファイル",
"masterEncryptionKeyVaultFilePath": "キー保存パス",
"showEncryptionStatus": "暗号化状態を表示",
"showEncryptionStatusDes": "有効にすると、ユーザーはファイル詳細でファイルの暗号化状態を確認できます。"
},
"giftCodes": {
"giftCodesSettings": "ギフトコード",
@ -1034,7 +1042,11 @@
"sharePointUrlDes": "SharePointサイトのURLを入力してください。フォーカスが外れると、システムが自動的に正しいドライブ識別子に変換します。",
"ks3selectRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>BEIJING</0>)。",
"ks3EndpointPathStyle": "パス形式エンドポイントの強制使用を選択してください。",
"ossRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>cn-hangzhou</0>)。<1>OSSリージョンとエンドポイント</1>の表で対応するリージョンを見つけ、対応する<2>リージョンID</2>を入力できます。"
"ossRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>cn-hangzhou</0>)。<1>OSSリージョンとエンドポイント</1>の表で対応するリージョンを見つけ、対応する<2>リージョンID</2>を入力できます。",
"fileEncryption": "ファイル暗号化",
"enableFileEncryption": "ファイル暗号化を有効にする",
"enableFileEncryptionDes": "有効にすると、ファイルのBlobが暗号化されて保存されます。この設定の変更は新しく追加されたファイルにのみ有効です。",
"encryptedFile": "暗号化されたファイル"
},
"node": {
"slave": "スレーブ",

View File

@ -436,7 +436,12 @@
"viewSetting": "보기 설정",
"saved": "저장됨",
"notSet": "설정되지 않음",
"deleteViewSetting": "보기 설정 삭제"
"deleteViewSetting": "보기 설정 삭제",
"encryption": "암호화",
"fullEncryption": "{{cipher}}로 암호화됨",
"partialEncryption": "부분 암호화",
"partialEncryptionDes": "이 파일의 일부 버전 또는 기타 데이터가 암호화되지 않았습니다.",
"noEncryption": "암호화되지 않음"
},
"modals": {
"includePasswordInShareLink": "공유 링크에 비밀번호 포함",

View File

@ -716,7 +716,15 @@
"blobUrlCache": "Blob URL 캐시",
"clearBlobUrlCache": "Blob URL 캐시 지우기",
"clearBlobUrlCacheDes": "캐시 적중률을 높이기 위해 Cloudreve는 Blob URL을 캐시하고 재사용합니다. CDN 주소 등의 설정이 변경되면 캐시를 지워 주세요.",
"cacheCleared": "캐시가 지워졌습니다."
"cacheCleared": "캐시가 지워졌습니다.",
"masterEncryptionKeyVault": "마스터 암호화 키 저장 방식",
"masterEncryptionKeyVaultDes": "마스터 암호화 키의 저장 방식을 선택하며, 재시작 후 적용됩니다. 저장 방식이나 키를 변경해야 하는 경우 데이터 손실을 방지하기 위해 <0>문서</0>의 단계를 엄격히 따르십시오.",
"masterEncryptionKeyVaultSetting": "데이터베이스",
"masterEncryptionKeyVaultEnv": "환경 변수 <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "파일",
"masterEncryptionKeyVaultFilePath": "키 저장 경로",
"showEncryptionStatus": "암호화 상태 표시",
"showEncryptionStatusDes": "활성화하면 사용자가 파일 상세 정보에서 파일의 암호화 상태를 볼 수 있습니다."
},
"giftCodes": {
"giftCodesSettings": "기프트 코드",
@ -1034,7 +1042,11 @@
"sharePointUrlDes": "SharePoint 사이트 URL을 입력하세요. 포커스를 잃으면 시스템이 자동으로 올바른 드라이브 식별자로 변환합니다.",
"ks3selectRegionDes": "스토리지 버킷이 위치한 지역 코드를 입력하세요. 예: <0>BEIJING</0>.",
"ks3EndpointPathStyle": "경로 형식 Endpoint를 강제로 사용할지 선택하세요.",
"ossRegionDes": "버킷이 위치한 지역 코드를 입력하세요. 예: <0>cn-hangzhou</0>. <1>OSS 지역 및 엔드포인트</1> 표에서 해당 지역을 찾아 해당하는 <2>지역 ID</2>를 입력할 수 있습니다."
"ossRegionDes": "버킷이 위치한 지역 코드를 입력하세요. 예: <0>cn-hangzhou</0>. <1>OSS 지역 및 엔드포인트</1> 표에서 해당 지역을 찾아 해당하는 <2>지역 ID</2>를 입력할 수 있습니다.",
"fileEncryption": "파일 암호화",
"enableFileEncryption": "파일 암호화 활성화",
"enableFileEncryptionDes": "활성화하면 파일 Blob이 암호화되어 저장됩니다. 이 설정을 변경하면 새로 추가되는 파일에만 적용됩니다.",
"encryptedFile": "암호화된 파일"
},
"node": {
"slave": "슬레이브",

View File

@ -436,7 +436,12 @@
"viewSetting": "Configuração de visualização",
"saved": "Salvo",
"notSet": "Não definido",
"deleteViewSetting": "Excluir configuração de visualização"
"deleteViewSetting": "Excluir configuração de visualização",
"encryption": "Criptografia",
"fullEncryption": "Criptografado com {{cipher}}",
"partialEncryption": "Parcialmente criptografado",
"partialEncryptionDes": "Algumas versões ou outros dados deste arquivo não estão criptografados.",
"noEncryption": "Não criptografado"
},
"modals": {
"includePasswordInShareLink": "Incluir senha no link de compartilhamento",

View File

@ -717,7 +717,15 @@
"blobUrlCache": "Cache de URL Blob",
"clearBlobUrlCache": "Limpar cache de URL Blob",
"clearBlobUrlCacheDes": "Para aumentar a taxa de acerto do cache, o Cloudreve armazena em cache e reutiliza URLs Blob. Quando o endereço CDN ou outras configurações mudam, limpe o cache.",
"cacheCleared": "Cache limpo."
"cacheCleared": "Cache limpo.",
"masterEncryptionKeyVault": "Armazenamento da chave mestra de criptografia",
"masterEncryptionKeyVaultDes": "Escolha como armazenar a chave mestra de criptografia, entra em vigor após reiniciar. Se você precisar alterar o método de armazenamento ou a chave, siga estritamente as etapas na <0>documentação</0> para evitar perda de dados.",
"masterEncryptionKeyVaultSetting": "Banco de dados",
"masterEncryptionKeyVaultEnv": "Variável de ambiente <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "Arquivo",
"masterEncryptionKeyVaultFilePath": "Caminho de armazenamento da chave",
"showEncryptionStatus": "Mostrar status de criptografia",
"showEncryptionStatusDes": "Quando ativado, os usuários podem visualizar o status de criptografia dos arquivos nos detalhes do arquivo."
},
"giftCodes": {
"giftCodesSettings": "Códigos de Presente",
@ -1033,7 +1041,11 @@
"sharePointUrlDes": "Digite a URL do site SharePoint. Após perder o foco, o sistema converterá automaticamente para o identificador de driver correto.",
"ks3selectRegionDes": "Digite o código da região do bucket de armazenamento, ex. <0>BEIJING</0> .",
"ks3EndpointPathStyle": "Selecione o formato do endereço do Endpoint KS3.",
"ossRegionDes": "Digite o código da região onde está localizado o bucket, ex. <0>cn-hangzhou</0>. Você pode encontrar a região correspondente na tabela <1>Regiões e endpoints do OSS</1> e preencher o <2>ID da região</2> correspondente."
"ossRegionDes": "Digite o código da região onde está localizado o bucket, ex. <0>cn-hangzhou</0>. Você pode encontrar a região correspondente na tabela <1>Regiões e endpoints do OSS</1> e preencher o <2>ID da região</2> correspondente.",
"fileEncryption": "Criptografia de arquivos",
"enableFileEncryption": "Ativar criptografia de arquivos",
"enableFileEncryptionDes": "Quando ativado, os blobs de arquivos serão armazenados criptografados. Alterar esta configuração afeta apenas arquivos recém-adicionados.",
"encryptedFile": "Arquivos criptografados"
},
"node": {
"slave": "escravo",

View File

@ -436,7 +436,12 @@
"viewSetting": "Настройки просмотра",
"saved": "Сохранено",
"notSet": "Не установлено",
"deleteViewSetting": "Удалить настройки просмотра"
"deleteViewSetting": "Удалить настройки просмотра",
"encryption": "Шифрование",
"fullEncryption": "Зашифровано с помощью {{cipher}}",
"partialEncryption": "Частично зашифровано",
"partialEncryptionDes": "Некоторые версии или другие данные этого файла не зашифрованы.",
"noEncryption": "Не зашифровано"
},
"modals": {
"includePasswordInShareLink": "Включить пароль в ссылку на публикацию",

View File

@ -717,7 +717,15 @@
"blobUrlCache": "Кэш Blob URL",
"clearBlobUrlCache": "Очистить кэш Blob URL",
"clearBlobUrlCacheDes": "Для увеличения частоты попаданий в кэш Cloudreve кэширует и повторно использует Blob URL. При изменении настроек, таких как адрес CDN, пожалуйста, очистите кэш.",
"cacheCleared": "Кэш очищен"
"cacheCleared": "Кэш очищен",
"masterEncryptionKeyVault": "Хранилище главного ключа шифрования",
"masterEncryptionKeyVaultDes": "Выберите способ хранения главного ключа шифрования, вступает в силу после перезапуска. Если вам нужно изменить способ хранения или ключ, строго следуйте шагам в <0>документации</0>, чтобы избежать потери данных.",
"masterEncryptionKeyVaultSetting": "База данных",
"masterEncryptionKeyVaultEnv": "Переменная окружения <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "Файл",
"masterEncryptionKeyVaultFilePath": "Путь хранения ключа",
"showEncryptionStatus": "Показывать статус шифрования",
"showEncryptionStatusDes": "При включении пользователи могут просматривать статус шифрования файлов в детальной информации о файле."
},
"giftCodes": {
"giftCodesSettings": "Подарочные коды",
@ -1035,7 +1043,11 @@
"sharePointUrlDes": "Введите URL сайта SharePoint. После потери фокуса система автоматически преобразует его в правильный идентификатор диска.",
"ks3selectRegionDes": "Введите код региона, где находится корзина, например <0>BEIJING</0>.",
"ks3EndpointPathStyle": "Выберите, принудительно ли использовать Endpoint в формате пути.",
"ossRegionDes": "Введите код региона, где находится корзина, например <0>cn-hangzhou</0>. Вы можете найти соответствующий регион в таблице <1>Регионы и конечные точки OSS</1> и заполнить соответствующий <2>ID региона</2>."
"ossRegionDes": "Введите код региона, где находится корзина, например <0>cn-hangzhou</0>. Вы можете найти соответствующий регион в таблице <1>Регионы и конечные точки OSS</1> и заполнить соответствующий <2>ID региона</2>.",
"fileEncryption": "Шифрование файлов",
"enableFileEncryption": "Включить шифрование файлов",
"enableFileEncryptionDes": "При включении блобы файлов будут храниться в зашифрованном виде. Изменение этого параметра влияет только на вновь добавляемые файлы.",
"encryptedFile": "Зашифрованные файлы"
},
"node": {
"slave": "Подчиненный узел",

View File

@ -401,7 +401,12 @@
"viewSetting": "视图设置",
"saved": "已保存",
"notSet": "未设置",
"deleteViewSetting": "删除视图设置"
"deleteViewSetting": "删除视图设置",
"encryption": "加密",
"fullEncryption": "使用 {{cipher}} 加密",
"partialEncryption": "部分加密",
"partialEncryptionDes": "此文件的某些版本或其他数据未被加密。",
"noEncryption": "未加密"
},
"modals": {
"includePasswordInShareLink": "在链接中包含密码",

View File

@ -716,7 +716,15 @@
"blobUrlCache": "Blob URL 缓存",
"clearBlobUrlCache": "清除 Blob URL 缓存",
"clearBlobUrlCacheDes": "为了增加缓存命中率Cloudreve 会缓存并复用 Blob URL。当 CDN 地址等设置发生变更时,请清除缓存。",
"cacheCleared": "缓存已清除"
"cacheCleared": "缓存已清除",
"masterEncryptionKeyVault": "主加密密钥存储方式",
"masterEncryptionKeyVaultDes": "选择主加密密钥的存储方式,重启后生效。如果需要更换存储方式或密钥,请严格遵循<0>文档</0>中的步骤,避免数据丢失。",
"masterEncryptionKeyVaultSetting": "数据库",
"masterEncryptionKeyVaultEnv": "环境变量 <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "文件",
"masterEncryptionKeyVaultFilePath": "密钥存储路径",
"showEncryptionStatus": "显示加密状态",
"showEncryptionStatusDes": "开启后,用户在文件详情中可以查看文件的加密状态。"
},
"giftCodes": {
"giftCodesSettings": "礼品码",
@ -1034,7 +1042,11 @@
"sharePointUrlDes": "输入 SharePoint 站点 URL。失去焦点后系统将自动转换为正确的驱动器标识。",
"ks3selectRegionDes": "输入存储桶所在的区域代码,如 <0>BEIJING</0>。",
"ks3EndpointPathStyle": "选择是否强制使用路径格式 Endpoint。",
"ossRegionDes": "输入存储桶所在的区域代码,如 <0>cn-hangzhou</0>。你可以在 <1>OSS地域和访问域名</1> 的表格中找到对应地域,并填写对应的 <2>地域ID</2>。"
"ossRegionDes": "输入存储桶所在的区域代码,如 <0>cn-hangzhou</0>。你可以在 <1>OSS地域和访问域名</1> 的表格中找到对应地域,并填写对应的 <2>地域ID</2>。",
"fileEncryption": "文件加密",
"enableFileEncryption": "启用文件加密",
"enableFileEncryptionDes": "开启后,文件 Blob 会被加密存储。更改此设置只对新增的文件有效。",
"encryptedFile": "加密的文件"
},
"node": {
"slave": "从机",

View File

@ -401,7 +401,12 @@
"viewSetting": "檢視設定",
"saved": "已保存",
"notSet": "未設定",
"deleteViewSetting": "刪除檢視設定"
"deleteViewSetting": "刪除檢視設定",
"encryption": "加密",
"fullEncryption": "使用 {{cipher}} 加密",
"partialEncryption": "部分加密",
"partialEncryptionDes": "此檔案的某些版本或其他資料未被加密。",
"noEncryption": "未加密"
},
"modals": {
"includePasswordInShareLink": "在連結中包含密碼",

View File

@ -716,7 +716,15 @@
"blobUrlCache": "Blob URL 快取",
"clearBlobUrlCache": "清除 Blob URL 快取",
"clearBlobUrlCacheDes": "為了增加快取命中率Cloudreve 會快取並復用 Blob URL。當 CDN 地址等設定發生變更時,請清除快取。",
"cacheCleared": "快取已清除"
"cacheCleared": "快取已清除",
"masterEncryptionKeyVault": "主加密金鑰儲存方式",
"masterEncryptionKeyVaultDes": "選擇主加密金鑰的儲存方式,重啟後生效。如果需要更換儲存方式或金鑰,請嚴格遵循<0>文件</0>中的步驟,避免資料遺失。",
"masterEncryptionKeyVaultSetting": "資料庫",
"masterEncryptionKeyVaultEnv": "環境變數 <0>CR_ENCRYPT_MASTER_KEY</0>",
"masterEncryptionKeyVaultFile": "檔案",
"masterEncryptionKeyVaultFilePath": "金鑰儲存路徑",
"showEncryptionStatus": "顯示加密狀態",
"showEncryptionStatusDes": "開啟後,使用者在檔案詳情中可以查看檔案的加密狀態。"
},
"giftCodes": {
"giftCodesSettings": "禮品碼",
@ -1034,7 +1042,11 @@
"sharePointUrlDes": "輸入 SharePoint 站點 URL。失去焦點後系統將自動轉換為正確的驅動器標識。",
"ks3selectRegionDes": "輸入儲存桶所在的區域程式碼,如 <0>BEIJING</0>。",
"ks3EndpointPathStyle": "選擇是否強制使用路徑格式 Endpoint。",
"ossRegionDes": "輸入儲存桶所在的區域代碼,如 <0>cn-hangzhou</0>。你可以在 <1>OSS地域和訪問域名</1> 的表格中找到對應地域,並填寫對應的 <2>地域ID</2>。"
"ossRegionDes": "輸入儲存桶所在的區域代碼,如 <0>cn-hangzhou</0>。你可以在 <1>OSS地域和訪問域名</1> 的表格中找到對應地域,並填寫對應的 <2>地域ID</2>。",
"fileEncryption": "檔案加密",
"enableFileEncryption": "啟用檔案加密",
"enableFileEncryptionDes": "開啟後,檔案 Blob 會被加密儲存。更改此設定只對新增的檔案有效。",
"encryptedFile": "加密的檔案"
},
"node": {
"slave": "從機",

View File

@ -1,4 +1,4 @@
import { EntityType, PaginationResults, PolicyType } from "./explorer.ts";
import { EncryptionCipher, EntityType, PaginationResults, PolicyType } from "./explorer.ts";
import { Capacity } from "./user.ts";
import { TaskStatus, TaskSummary, TaskType } from "./workflow.ts";
@ -235,6 +235,7 @@ export interface PolicySetting {
source_auth?: boolean;
qiniu_upload_cdn?: boolean;
chunk_concurrency?: number;
encryption?: boolean;
}
export interface User extends CommonMixin {
@ -409,11 +410,20 @@ export interface Entity extends CommonMixin {
storage_policy?: StoragePolicy;
file?: File[];
};
props?: EntityProps;
user_hash_id?: string;
user_hash_id_map?: Record<number, string>;
}
export interface EntityProps {
encrypt_metadata?: EncryptMetadata;
}
export interface EncryptMetadata {
algorithm: EncryptionCipher;
}
export interface Metadata extends CommonMixin {
name?: string;
value?: string;

View File

@ -68,6 +68,7 @@ export interface Entity {
storage_policy?: StoragePolicy;
size: number;
created_by?: User;
encrypted_with?: EncryptionCipher;
}
export interface Share {
@ -492,7 +493,7 @@ export interface CreateViewerSessionService {
version?: string;
}
export enum EncryptionAlgorithm {
export enum EncryptionCipher {
aes256ctr = "aes-256-ctr",
}
@ -506,11 +507,11 @@ export interface UploadSessionRequest {
[key: string]: string;
};
mime_type?: string;
encryption_supported?: EncryptionAlgorithm[];
encryption_supported?: EncryptionCipher[];
}
export interface EncryptMetadata {
algorithm: EncryptionAlgorithm;
algorithm: EncryptionCipher;
key_plain_text: string;
iv: string;
}

View File

@ -46,6 +46,7 @@ export interface SiteConfig {
custom_nav_items?: CustomNavItem[];
custom_html?: CustomHTML;
thumb_exts?: string[];
show_encryption_status?: boolean;
}
export interface CaptchaResponse {

View File

@ -8,6 +8,7 @@ import { useAppDispatch } from "../../../../redux/hooks";
import { sizeToString } from "../../../../util";
import { NoWrapTypography } from "../../../Common/StyledComponents";
import UserAvatar from "../../../Common/User/UserAvatar";
import { EncryptionStatusText } from "../../../FileManager/Sidebar/BasicInfo";
import { EntityTypeText } from "../../../FileManager/Sidebar/Data";
import SettingForm from "../../../Pages/Setting/SettingForm";
import UserDialog from "../../User/UserDialog/UserDialog";
@ -102,6 +103,14 @@ const EntityForm = ({ values }: { values: Entity }) => {
</Link>
</Typography>
</SettingForm>
<SettingForm title={t("application:fileManager.encryption")} noContainer lgWidth={4}>
<EncryptionStatusText
status={{
status: values.props?.encrypt_metadata?.algorithm ? "full" : "none",
cipher: values.props?.encrypt_metadata?.algorithm ? [values.props?.encrypt_metadata?.algorithm] : [],
}}
/>
</SettingForm>
<SettingForm title={t("entity.referredFiles")} noContainer lgWidth={12}>
<EntityFileList files={values.edges?.file ?? []} userHashIDMap={values.user_hash_id_map ?? {}} />
</SettingForm>

View File

@ -10,9 +10,11 @@ import { sizeToString } from "../../../util";
import { NoWrapTableCell, NoWrapTypography, SquareChip } from "../../Common/StyledComponents";
import TimeBadge from "../../Common/TimeBadge";
import UserAvatar from "../../Common/User/UserAvatar";
import { cipherDisplayName } from "../../FileManager/Sidebar/BasicInfo";
import { EntityTypeText } from "../../FileManager/Sidebar/Data";
import Delete from "../../Icons/Delete";
import Download from "../../Icons/Download";
import ShieldLock from "../../Icons/ShieldLock";
export interface EntityRowProps {
entity?: Entity;
@ -47,15 +49,15 @@ const EntityRow = ({
const onOpenClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setOpenLoading(true);
dispatch(getEntityUrl(entity?.id ?? 0))
.then((url) => {
// 直接下载文件使用a标签的download属性强制下载
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = `entity-${entity?.id}`;
link.style.display = 'none';
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@ -64,7 +66,7 @@ const EntityRow = ({
setOpenLoading(false);
})
.catch((error) => {
console.error('Failed to get entity URL:', error);
console.error("Failed to get entity URL:", error);
});
};
@ -144,6 +146,21 @@ const EntityRow = ({
</Tooltip>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
{!entity?.reference_count && <SquareChip size="small" label={t("entity.waitForRecycle")} />}
{entity?.props?.encrypt_metadata?.algorithm && (
<Tooltip
title={t("application:fileManager.fullEncryption", {
cipher: cipherDisplayName(entity?.props?.encrypt_metadata?.algorithm),
})}
>
<ShieldLock
sx={{
width: "20px",
height: "20px",
color: (theme) => theme.palette.success.main,
}}
/>
</Tooltip>
)}
</Box>
</Box>
</NoWrapTableCell>

View File

@ -11,6 +11,7 @@ import {
StyledTableContainerPaper,
} from "../../../Common/StyledComponents";
import TimeBadge from "../../../Common/TimeBadge";
import { EncryptionStatusText } from "../../../FileManager/Sidebar/BasicInfo";
import { EntityTypeText } from "../../../FileManager/Sidebar/Data";
import EntityDialog from "../../Entity/EntityDialog/EntityDialog";
import UserDialog from "../../User/UserDialog/UserDialog";
@ -53,6 +54,7 @@ const FileEntity = () => {
<NoWrapTableCell width={100}>{t("file.size")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("file.storagePolicy")}</NoWrapTableCell>
<NoWrapTableCell width={200}>{t("file.source")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("application:fileManager.encryption")}</NoWrapTableCell>
<NoWrapTableCell width={200}>{t("file.createdAt")}</NoWrapTableCell>
<NoWrapTableCell width={150}>{t("file.creator")}</NoWrapTableCell>
</TableRow>
@ -91,6 +93,18 @@ const FileEntity = () => {
<NoWrapTypography variant="inherit">{option.source ?? ""}</NoWrapTypography>
</Tooltip>
</TableCell>
<TableCell>
<EncryptionStatusText
flexWrap={false}
simplified
status={{
status: option.props?.encrypt_metadata?.algorithm ? "full" : "none",
cipher: option.props?.encrypt_metadata?.algorithm
? [option.props?.encrypt_metadata?.algorithm]
: [],
}}
/>
</TableCell>
<TableCell>
<NoWrapTypography variant="inherit">
<TimeBadge datetime={option.created_at ?? ""} variant="inherit" timeAgoThreshold={0} />

View File

@ -14,6 +14,7 @@ import TimeBadge from "../../Common/TimeBadge";
import UserAvatar from "../../Common/User/UserAvatar";
import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon";
import UploadingTag from "../../FileManager/Explorer/UploadingTag";
import { EncryptionStatus, EncryptionStatusText } from "../../FileManager/Sidebar/BasicInfo";
import Delete from "../../Icons/Delete";
import LinkIcon from "../../Icons/LinkOutlined";
import Open from "../../Icons/Open";
@ -69,7 +70,7 @@ const FileRow = ({
const onOpenClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setOpenLoading(true);
dispatch(getFileUrl(file?.id ?? 0))
.then((url) => {
const ext = fileExtension(file?.name ?? "");
@ -77,24 +78,24 @@ const FileRow = ({
let hasViewer = false;
try {
// check Viewers object is loaded and valid
if (ext && Viewers && typeof Viewers === 'object' && Viewers[ext]) {
if (ext && Viewers && typeof Viewers === "object" && Viewers[ext]) {
hasViewer = Array.isArray(Viewers[ext]) && Viewers[ext].length > 0;
}
} catch (error) {
console.warn('Failed to check viewer availability:', error);
console.warn("Failed to check viewer availability:", error);
hasViewer = false;
}
if (hasViewer) {
// 可预览文件:新窗口打开预览,窗口保持显示预览内容
window.open(url, "_blank");
} else {
// 下载文件使用a标签的download属性强制下载
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = file?.name || `file-${file?.id}`;
link.style.display = 'none';
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@ -104,7 +105,7 @@ const FileRow = ({
setOpenLoading(false);
})
.catch((error) => {
console.error('Failed to get file URL:', error);
console.error("Failed to get file URL:", error);
});
};
@ -168,6 +169,23 @@ const FileRow = ({
return sizeToString(file?.edges?.entities?.reduce((acc, entity) => acc + (entity.size ?? 0), 0) ?? 0);
}, [file?.edges?.entities]);
const encryptionStatus = useMemo(() => {
const status: EncryptionStatus = { status: "none", cipher: [] };
let encrypted = 0;
file?.edges?.entities?.forEach((entity) => {
if (entity.props?.encrypt_metadata?.algorithm) {
encrypted++;
if (!status.cipher.includes(entity.props?.encrypt_metadata?.algorithm)) {
status.cipher.push(entity.props?.encrypt_metadata?.algorithm);
}
}
});
if (encrypted > 0) {
status.status = encrypted === file?.edges?.entities?.length ? "full" : "partial";
}
return status;
}, [file?.edges?.entities]);
return (
<TableRow hover key={file?.id} sx={{ cursor: "pointer" }} onClick={onRowClick} selected={selected}>
<TableCell padding="checkbox">
@ -203,6 +221,9 @@ const FileRow = ({
</Box>
</Tooltip>
)}
{encryptionStatus && encryptionStatus.status !== "none" && (
<EncryptionStatusText simplified flexWrap={false} status={encryptionStatus} />
)}
</Box>
</Box>
</NoWrapTableCell>

View File

@ -13,7 +13,7 @@ import PageHeader, { PageTabQuery } from "../../Pages/PageHeader.tsx";
import SettingsWrapper from "../Settings/SettingWrapper.tsx";
import CustomPropsSetting from "./CustomProps/CustomPropsSetting.tsx";
import FileIcons from "./Icons/FileIcons.tsx";
import Parameters from "./Parameters.tsx";
import Parameters from "./Parameters/Parameters.tsx";
import ViewerSetting from "./ViewerSetting/ViewerSetting.tsx";
export enum SettingsPageTab {
@ -102,6 +102,9 @@ const FileSystem = () => {
"viewer_session_timeout",
"entity_url_default_ttl",
"entity_url_cache_margin",
"encrypt_master_key_vault",
"encrypt_master_key_file",
"show_encryption_status",
]}
>
<Parameters />

View File

@ -1,610 +0,0 @@
import { DeleteOutline } from "@mui/icons-material";
import {
Box,
Collapse,
FormControl,
FormControlLabel,
Link,
ListItemText,
Stack,
Switch,
Typography,
} from "@mui/material";
import { useSnackbar } from "notistack";
import * as React from "react";
import { useCallback, useContext, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { sendClearBlobUrlCache } from "../../../api/api.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import { isTrueVal } from "../../../session/utils.ts";
import SizeInput from "../../Common/SizeInput.tsx";
import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx";
import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../Common/StyledComponents.tsx";
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
import SettingForm from "../../Pages/Setting/SettingForm.tsx";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings/Settings.tsx";
import { SettingContext } from "../Settings/SettingWrapper.tsx";
const Parameters = () => {
const { t } = useTranslation("dashboard");
const { formRef, setSettings, values } = useContext(SettingContext);
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const clearBlobUrlCache = () => {
setLoading(true);
dispatch(sendClearBlobUrlCache())
.then(() => {
setLoading(false);
enqueueSnackbar(t("settings.cacheCleared"), { variant: "success", action: DefaultCloseAction });
})
.catch(() => {
setLoading(false);
});
};
const onMimeMappingChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSettings({
mime_mapping: e.target.value,
});
}, []);
return (
<Box component={"form"} ref={formRef} onSubmit={(e) => e.preventDefault()}>
<Stack spacing={5}>
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("nav.fileSystem")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("settings.textEditMaxSize")} lgWidth={5}>
<FormControl>
<SizeInput
variant={"outlined"}
required
allowZero={false}
value={parseInt(values.maxEditSize) ?? 0}
onChange={(e) =>
setSettings({
maxEditSize: e.toString(),
})
}
/>
<NoMarginHelperText>{t("settings.textEditMaxSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.trashBinInterval")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
value={values.cron_trash_bin_collect}
onChange={(e) =>
setSettings({
cron_trash_bin_collect: e.target.value,
})
}
required
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.cronDes"
values={{
des: t("settings.trashBinIntervalDes"),
}}
ns={"dashboard"}
components={[<Link href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.entityCollectInterval")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
value={values.cron_entity_collect}
onChange={(e) =>
setSettings({
cron_entity_collect: e.target.value,
})
}
required
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.cronDes"
ns={"dashboard"}
values={{
des: t("settings.entityCollectIntervalDes"),
}}
components={[<Link href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.publicResourceMaxAge")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.public_resource_maxage}
onChange={(e) =>
setSettings({
public_resource_maxage: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.publicResourceMaxAgeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.defaultPagination")} lgWidth={5}>
<FormControl fullWidth>
<DenseSelect
renderValue={(v) => (
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{v == "0" ? t("settings.offsetPagination") : t("settings.cursorPagination")}
</ListItemText>
)}
onChange={(e) =>
setSettings({
use_cursor_pagination: e.target.value as string,
})
}
MenuProps={{
PaperProps: { sx: { maxWidth: 230 } },
MenuListProps: {
sx: {
"& .MuiMenuItem-root": {
whiteSpace: "normal",
},
},
},
}}
value={values.use_cursor_pagination}
>
<SquareMenuItem value={"0"}>
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Typography variant={"body2"} fontWeight={600}>
{t("settings.offsetPagination")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("settings.offsetPaginationDes")}
</Typography>
</Box>
</SquareMenuItem>
<SquareMenuItem value={"1"}>
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography variant={"body2"} fontWeight={600}>
{t("settings.cursorPagination")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("settings.cursorPaginationDes")}
</Typography>
</Box>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.defaultPaginationDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.maxPageSize")} lgWidth={5}>
<FormControl>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.max_page_size}
onChange={(e) =>
setSettings({
max_page_size: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.maxPageSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.maxBatchSize")} lgWidth={5}>
<FormControl>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.max_batched_file}
onChange={(e) =>
setSettings({
max_batched_file: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.maxBatchSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.maxRecursiveSearch")} lgWidth={5}>
<FormControl>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.max_recursive_searched_folder}
onChange={(e) =>
setSettings({
max_recursive_searched_folder: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.maxRecursiveSearchDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.mapProvider")} lgWidth={5}>
<FormControl>
<DenseSelect
onChange={(e) =>
setSettings({
map_provider: e.target.value as string,
})
}
value={values.map_provider}
>
<SquareMenuItem value={"google"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.mapGoogle")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"openstreetmap"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.mapOpenStreetMap")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"mapbox"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.mapboxMap")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.mapProviderDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<Collapse in={values.map_provider === "mapbox"} unmountOnExit>
<SettingForm title={t("settings.mapboxAccessToken")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
fullWidth
required
value={values.map_mapbox_ak ?? ""}
onChange={(e) => setSettings({ map_mapbox_ak: e.target.value })}
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.mapboxAccessTokenDes"
ns="dashboard"
components={[<Link href="https://account.mapbox.com/access-tokens" target="_blank" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
</Collapse>
<Collapse in={values.map_provider === "google"} unmountOnExit>
<SettingForm title={t("settings.tileType")} lgWidth={5}>
<FormControl>
<DenseSelect
onChange={(e) =>
setSettings({
map_google_tile_type: e.target.value as string,
})
}
value={values.map_google_tile_type}
>
<SquareMenuItem value={"terrain"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.tileTypeTerrain")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"satellite"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.tileTypeSatellite")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"regular"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.tileTypeGeneral")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.tileTypeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</Collapse>
<SettingForm title={t("settings.mimeMapping")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
multiline
rows={6}
value={values.mime_mapping}
onChange={onMimeMappingChange}
required
/>
<NoMarginHelperText>{t("settings.mimeMappingDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</SettingSectionContent>
</SettingSection>
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("settings.searchQuery")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("application:navbar.photos")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_image_query}
onChange={(e) =>
setSettings({
explorer_category_image_query: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("application:navbar.videos")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_video_query}
onChange={(e) =>
setSettings({
explorer_category_video_query: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("application:navbar.music")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_audio_query}
onChange={(e) =>
setSettings({
explorer_category_audio_query: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("application:navbar.documents")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_document_query}
onChange={(e) =>
setSettings({
explorer_category_document_query: e.target.value,
})
}
required
/>
</SettingForm>
</SettingSectionContent>
</SettingSection>
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("settings.advanceOptions")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("settings.archiveTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.archive_timeout}
onChange={(e) =>
setSettings({
archive_timeout: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("settings.uploadSessionTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.upload_session_timeout}
onChange={(e) =>
setSettings({
upload_session_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.uploadSessionDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.slaveAPIExpiration")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.slave_api_timeout}
onChange={(e) =>
setSettings({
slave_api_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.slaveAPIExpirationDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.folderPropsTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.folder_props_timeout}
onChange={(e) =>
setSettings({
folder_props_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.folderPropsTimeoutDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.failedChunkRetry")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.chunk_retries}
onChange={(e) =>
setSettings({
chunk_retries: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.failedChunkRetryDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm lgWidth={5}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={isTrueVal(values.use_temp_chunk_buffer)}
onChange={(e) =>
setSettings({
use_temp_chunk_buffer: e.target.checked ? "1" : "0",
})
}
/>
}
label={t("settings.cacheChunks")}
/>
<NoMarginHelperText>{t("settings.cacheChunksDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.transitParallelNum")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.max_parallel_transfer}
onChange={(e) =>
setSettings({
max_parallel_transfer: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.transitParallelNumDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.oauthRefresh")} lgWidth={5}>
<DenseFilledTextField
fullWidth
required
value={values.cron_oauth_cred_refresh}
onChange={(e) =>
setSettings({
cron_oauth_cred_refresh: e.target.value,
})
}
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.cronDes"
values={{
des: t("settings.oauthRefreshDes"),
}}
ns={"dashboard"}
components={[<Link href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" />]}
/>
</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.wopiSessionTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.viewer_session_timeout}
onChange={(e) =>
setSettings({
viewer_session_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.wopiSessionTimeoutDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.fileBlobTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.entity_url_default_ttl}
onChange={(e) =>
setSettings({
entity_url_default_ttl: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.fileBlobTimeoutDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.fileBlobMargin")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.entity_url_cache_margin}
onChange={(e) =>
setSettings({
entity_url_cache_margin: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.fileBlobMarginDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.blobUrlCache")} lgWidth={5} anchorId="clearBlobUrlCache">
<FormControl fullWidth>
<Box>
<SecondaryButton
startIcon={<DeleteOutline />}
variant="contained"
loading={loading}
color="primary"
onClick={clearBlobUrlCache}
>
{t("settings.clearBlobUrlCache")}
</SecondaryButton>
</Box>
<NoMarginHelperText>{t("settings.clearBlobUrlCacheDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</SettingSectionContent>
</SettingSection>
</Stack>
</Box>
);
};
export default Parameters;

View File

@ -0,0 +1,226 @@
import { DeleteOutline } from "@mui/icons-material";
import { Box, FormControl, FormControlLabel, Link, Switch, Typography } from "@mui/material";
import { useSnackbar } from "notistack";
import { useContext, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { sendClearBlobUrlCache } from "../../../../api/api.ts";
import { useAppDispatch } from "../../../../redux/hooks.ts";
import { isTrueVal } from "../../../../session/utils.ts";
import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx";
import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings.tsx";
import { SettingContext } from "../../Settings/SettingWrapper.tsx";
const AdvancedOptionsSection = () => {
const { t } = useTranslation("dashboard");
const { setSettings, values } = useContext(SettingContext);
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const clearBlobUrlCache = () => {
setLoading(true);
dispatch(sendClearBlobUrlCache())
.then(() => {
setLoading(false);
enqueueSnackbar(t("settings.cacheCleared"), { variant: "success", action: DefaultCloseAction });
})
.catch(() => {
setLoading(false);
});
};
return (
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("settings.advanceOptions")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("settings.archiveTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.archive_timeout}
onChange={(e) =>
setSettings({
archive_timeout: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("settings.uploadSessionTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.upload_session_timeout}
onChange={(e) =>
setSettings({
upload_session_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.uploadSessionDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.slaveAPIExpiration")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.slave_api_timeout}
onChange={(e) =>
setSettings({
slave_api_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.slaveAPIExpirationDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.folderPropsTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.folder_props_timeout}
onChange={(e) =>
setSettings({
folder_props_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.folderPropsTimeoutDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.failedChunkRetry")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.chunk_retries}
onChange={(e) =>
setSettings({
chunk_retries: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.failedChunkRetryDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm lgWidth={5}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={isTrueVal(values.use_temp_chunk_buffer)}
onChange={(e) =>
setSettings({
use_temp_chunk_buffer: e.target.checked ? "1" : "0",
})
}
/>
}
label={t("settings.cacheChunks")}
/>
<NoMarginHelperText>{t("settings.cacheChunksDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.transitParallelNum")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.max_parallel_transfer}
onChange={(e) =>
setSettings({
max_parallel_transfer: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.transitParallelNumDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.oauthRefresh")} lgWidth={5}>
<DenseFilledTextField
fullWidth
required
value={values.cron_oauth_cred_refresh}
onChange={(e) =>
setSettings({
cron_oauth_cred_refresh: e.target.value,
})
}
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.cronDes"
values={{
des: t("settings.oauthRefreshDes"),
}}
ns={"dashboard"}
components={[<Link href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" />]}
/>
</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.wopiSessionTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.viewer_session_timeout}
onChange={(e) =>
setSettings({
viewer_session_timeout: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.wopiSessionTimeoutDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.fileBlobTimeout")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.entity_url_default_ttl}
onChange={(e) =>
setSettings({
entity_url_default_ttl: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.fileBlobTimeoutDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.fileBlobMargin")} lgWidth={5}>
<DenseFilledTextField
type="number"
inputProps={{ min: 1, setp: 1 }}
value={values.entity_url_cache_margin}
onChange={(e) =>
setSettings({
entity_url_cache_margin: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.fileBlobMarginDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("settings.blobUrlCache")} lgWidth={5} anchorId="clearBlobUrlCache">
<FormControl fullWidth>
<Box>
<SecondaryButton
startIcon={<DeleteOutline />}
variant="contained"
loading={loading}
color="primary"
onClick={clearBlobUrlCache}
>
{t("settings.clearBlobUrlCache")}
</SecondaryButton>
</Box>
<NoMarginHelperText>{t("settings.clearBlobUrlCacheDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</SettingSectionContent>
</SettingSection>
);
};
export default AdvancedOptionsSection;

View File

@ -0,0 +1,106 @@
import { Collapse, FormControl, FormControlLabel, Link, ListItemText, Switch, Typography } from "@mui/material";
import { useContext } from "react";
import { Trans, useTranslation } from "react-i18next";
import { isTrueVal } from "../../../../session/utils.ts";
import { Code } from "../../../Common/Code.tsx";
import { DenseFilledTextField, DenseSelect } from "../../../Common/StyledComponents.tsx";
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings.tsx";
import { SettingContext } from "../../Settings/SettingWrapper.tsx";
const FileEncryptionSection = () => {
const { t } = useTranslation("dashboard");
const { setSettings, values } = useContext(SettingContext);
return (
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("policy.fileEncryption")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("settings.masterEncryptionKeyVault")} lgWidth={5}>
<FormControl>
<DenseSelect
onChange={(e) =>
setSettings({
encrypt_master_key_vault: e.target.value as string,
})
}
value={values.encrypt_master_key_vault}
>
<SquareMenuItem value={"setting"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.masterEncryptionKeyVaultSetting")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"file"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.masterEncryptionKeyVaultFile")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"env"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{<Trans i18nKey="settings.masterEncryptionKeyVaultEnv" ns="dashboard" components={[<Code />]} />}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>
<Trans
i18nKey="settings.masterEncryptionKeyVaultDes"
ns="dashboard"
components={[
<Link
href="https://docs.cloudreve.org/usage/file-encryption#rotate-master-encryption-key"
target="_blank"
/>,
]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
<Collapse in={values.encrypt_master_key_vault === "file"} unmountOnExit>
<SettingForm title={t("settings.masterEncryptionKeyVaultFilePath")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
required
value={values.encrypt_master_key_file}
onChange={(e) => setSettings({ encrypt_master_key_file: e.target.value })}
/>
</FormControl>
</SettingForm>
</Collapse>
<SettingForm lgWidth={5}>
<FormControl>
<FormControlLabel
control={
<Switch
checked={isTrueVal(values.show_encryption_status)}
onChange={(e) => setSettings({ show_encryption_status: e.target.checked ? "1" : "0" })}
/>
}
label={t("settings.showEncryptionStatus")}
/>
<NoMarginHelperText>
<Trans i18nKey="settings.showEncryptionStatusDes" ns="dashboard" />
</NoMarginHelperText>
</FormControl>
</SettingForm>
</SettingSectionContent>
</SettingSection>
);
};
export default FileEncryptionSection;

View File

@ -0,0 +1,332 @@
import { Box, Collapse, FormControl, Link, ListItemText, Typography } from "@mui/material";
import * as React from "react";
import { useCallback, useContext } from "react";
import { Trans, useTranslation } from "react-i18next";
import SizeInput from "../../../Common/SizeInput.tsx";
import { DenseFilledTextField, DenseSelect } from "../../../Common/StyledComponents.tsx";
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings.tsx";
import { SettingContext } from "../../Settings/SettingWrapper.tsx";
const FileSystemSection = () => {
const { t } = useTranslation("dashboard");
const { setSettings, values } = useContext(SettingContext);
const onMimeMappingChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSettings({
mime_mapping: e.target.value,
});
}, []);
return (
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("nav.fileSystem")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("settings.textEditMaxSize")} lgWidth={5}>
<FormControl>
<SizeInput
variant={"outlined"}
required
allowZero={false}
value={parseInt(values.maxEditSize) ?? 0}
onChange={(e) =>
setSettings({
maxEditSize: e.toString(),
})
}
/>
<NoMarginHelperText>{t("settings.textEditMaxSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.trashBinInterval")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
value={values.cron_trash_bin_collect}
onChange={(e) =>
setSettings({
cron_trash_bin_collect: e.target.value,
})
}
required
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.cronDes"
values={{
des: t("settings.trashBinIntervalDes"),
}}
ns={"dashboard"}
components={[<Link href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.entityCollectInterval")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
value={values.cron_entity_collect}
onChange={(e) =>
setSettings({
cron_entity_collect: e.target.value,
})
}
required
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.cronDes"
ns={"dashboard"}
values={{
des: t("settings.entityCollectIntervalDes"),
}}
components={[<Link href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.publicResourceMaxAge")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.public_resource_maxage}
onChange={(e) =>
setSettings({
public_resource_maxage: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.publicResourceMaxAgeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.defaultPagination")} lgWidth={5}>
<FormControl fullWidth>
<DenseSelect
renderValue={(v) => (
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{v == "0" ? t("settings.offsetPagination") : t("settings.cursorPagination")}
</ListItemText>
)}
onChange={(e) =>
setSettings({
use_cursor_pagination: e.target.value as string,
})
}
MenuProps={{
PaperProps: { sx: { maxWidth: 230 } },
MenuListProps: {
sx: {
"& .MuiMenuItem-root": {
whiteSpace: "normal",
},
},
},
}}
value={values.use_cursor_pagination}
>
<SquareMenuItem value={"0"}>
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Typography variant={"body2"} fontWeight={600}>
{t("settings.offsetPagination")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("settings.offsetPaginationDes")}
</Typography>
</Box>
</SquareMenuItem>
<SquareMenuItem value={"1"}>
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography variant={"body2"} fontWeight={600}>
{t("settings.cursorPagination")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("settings.cursorPaginationDes")}
</Typography>
</Box>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.defaultPaginationDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.maxPageSize")} lgWidth={5}>
<FormControl>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.max_page_size}
onChange={(e) =>
setSettings({
max_page_size: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.maxPageSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.maxBatchSize")} lgWidth={5}>
<FormControl>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.max_batched_file}
onChange={(e) =>
setSettings({
max_batched_file: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.maxBatchSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.maxRecursiveSearch")} lgWidth={5}>
<FormControl>
<DenseFilledTextField
type="number"
inputProps={{ min: 0, setp: 1 }}
value={values.max_recursive_searched_folder}
onChange={(e) =>
setSettings({
max_recursive_searched_folder: e.target.value,
})
}
required
/>
<NoMarginHelperText>{t("settings.maxRecursiveSearchDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("settings.mapProvider")} lgWidth={5}>
<FormControl>
<DenseSelect
onChange={(e) =>
setSettings({
map_provider: e.target.value as string,
})
}
value={values.map_provider}
>
<SquareMenuItem value={"google"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.mapGoogle")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"openstreetmap"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.mapOpenStreetMap")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"mapbox"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.mapboxMap")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.mapProviderDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<Collapse in={values.map_provider === "mapbox"} unmountOnExit>
<SettingForm title={t("settings.mapboxAccessToken")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
fullWidth
required
value={values.map_mapbox_ak ?? ""}
onChange={(e) => setSettings({ map_mapbox_ak: e.target.value })}
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.mapboxAccessTokenDes"
ns="dashboard"
components={[<Link href="https://account.mapbox.com/access-tokens" target="_blank" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingForm>
</Collapse>
<Collapse in={values.map_provider === "google"} unmountOnExit>
<SettingForm title={t("settings.tileType")} lgWidth={5}>
<FormControl>
<DenseSelect
onChange={(e) =>
setSettings({
map_google_tile_type: e.target.value as string,
})
}
value={values.map_google_tile_type}
>
<SquareMenuItem value={"terrain"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.tileTypeTerrain")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"satellite"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.tileTypeSatellite")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value={"regular"}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.tileTypeGeneral")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.tileTypeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</Collapse>
<SettingForm title={t("settings.mimeMapping")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
multiline
rows={6}
value={values.mime_mapping}
onChange={onMimeMappingChange}
required
/>
<NoMarginHelperText>{t("settings.mimeMappingDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</SettingSectionContent>
</SettingSection>
);
};
export default FileSystemSection;

View File

@ -0,0 +1,24 @@
import { Box, Stack } from "@mui/material";
import { useContext } from "react";
import { SettingContext } from "../../Settings/SettingWrapper.tsx";
import AdvancedOptionsSection from "./AdvancedOptionsSection.tsx";
import FileEncryptionSection from "./FileEncryptionSection.tsx";
import FileSystemSection from "./FileSystemSection.tsx";
import SearchQuerySection from "./SearchQuerySection.tsx";
const Parameters = () => {
const { formRef } = useContext(SettingContext);
return (
<Box component={"form"} ref={formRef} onSubmit={(e) => e.preventDefault()}>
<Stack spacing={5}>
<FileSystemSection />
<SearchQuerySection />
<FileEncryptionSection />
<AdvancedOptionsSection />
</Stack>
</Box>
);
};
export default Parameters;

View File

@ -0,0 +1,72 @@
import { Typography } from "@mui/material";
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { SettingSection, SettingSectionContent } from "../../Settings/Settings.tsx";
import { SettingContext } from "../../Settings/SettingWrapper.tsx";
const SearchQuerySection = () => {
const { t } = useTranslation("dashboard");
const { setSettings, values } = useContext(SettingContext);
return (
<SettingSection>
<Typography variant="h6" gutterBottom>
{t("settings.searchQuery")}
</Typography>
<SettingSectionContent>
<SettingForm title={t("application:navbar.photos")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_image_query}
onChange={(e) =>
setSettings({
explorer_category_image_query: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("application:navbar.videos")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_video_query}
onChange={(e) =>
setSettings({
explorer_category_video_query: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("application:navbar.music")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_audio_query}
onChange={(e) =>
setSettings({
explorer_category_audio_query: e.target.value,
})
}
required
/>
</SettingForm>
<SettingForm title={t("application:navbar.documents")}>
<DenseFilledTextField
fullWidth
value={values.explorer_category_document_query}
onChange={(e) =>
setSettings({
explorer_category_document_query: e.target.value,
})
}
required
/>
</SettingForm>
</SettingSectionContent>
</SettingSection>
);
};
export default SearchQuerySection;

View File

@ -0,0 +1,49 @@
import { Box, FormControl, FormControlLabel, IconButton, Switch, Typography } from "@mui/material";
import { useCallback, useContext } from "react";
import { Trans, useTranslation } from "react-i18next";
import { StoragePolicy } from "../../../../../api/dashboard";
import QuestionCircle from "../../../../Icons/QuestionCircle";
import SettingForm from "../../../../Pages/Setting/SettingForm";
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings";
import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper";
const EncryptionSection = () => {
const { t } = useTranslation("dashboard");
const { values, setPolicy } = useContext(StoragePolicySettingContext);
const onEncryptionChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPolicy((p: StoragePolicy) => ({
...p,
settings: { ...p.settings, encryption: e.target.checked ? true : undefined },
}));
},
[setPolicy],
);
return (
<SettingSection>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6">{t("policy.fileEncryption")}</Typography>
<IconButton onClick={() => window.open("https://docs.cloudreve.org/usage/file-encryption", "_blank")}>
<QuestionCircle />
</IconButton>
</Box>
<SettingSectionContent>
<SettingForm lgWidth={5}>
<FormControl fullWidth>
<FormControlLabel
control={<Switch checked={values.settings?.encryption ?? false} onChange={onEncryptionChange} />}
label={t("policy.enableFileEncryption")}
/>
<NoMarginHelperText>
<Trans i18nKey="policy.enableFileEncryptionDes" ns="dashboard" />
</NoMarginHelperText>
</FormControl>
</SettingForm>
</SettingSectionContent>
</SettingSection>
);
};
export default EncryptionSection;

View File

@ -1,5 +1,6 @@
export { default as BasicInfoSection } from "./BasicInfoSection";
export { default as DownloadSection } from "./DownloadSection";
export { default as EncryptionSection } from "./EncryptionSection";
export * from "./magicVars";
export { default as MediaMetadataSection } from "./MediaMetadataSection";
export { default as StorageAndUploadSection } from "./StorageAndUploadSection";

View File

@ -5,6 +5,7 @@ import { Link as RouterLink } from "react-router-dom";
import {
BasicInfoSection,
DownloadSection,
EncryptionSection,
MediaMetadataSection,
StorageAndUploadSection,
ThumbnailsSection,
@ -32,6 +33,7 @@ const StoragePolicyForm = () => {
<DownloadSection />
<ThumbnailsSection />
<MediaMetadataSection />
<EncryptionSection />
</Stack>
</Box>
);

View File

@ -25,6 +25,7 @@ enum Source {
dav = "dav",
web_edit = "web_edit",
wopi = "wopi",
encrypted_file = "encrypted_file",
}
enum Node {
@ -134,7 +135,7 @@ export const TrafficDiagram = ({
res.push(Node.cloudreve);
}
} else {
if (proxyed || source == Source.wopi) {
if (proxyed || source == Source.wopi || source == Source.encrypted_file) {
res.push(Node.cloudreve);
}
@ -145,7 +146,12 @@ export const TrafficDiagram = ({
if (variant == "upload" && internalEndpoint && (source == Source.dav || source == Source.web_edit || proxyed)) {
res.push(Node.storage_node_internal);
} else if (variant == "download" && internalEndpoint && (source == Source.wopi || proxyed) && !cdn) {
} else if (
variant == "download" &&
internalEndpoint &&
(source == Source.wopi || proxyed || source == Source.encrypted_file) &&
!cdn
) {
res.push(Node.storage_node_internal);
} else {
res.push(Node.storage_node);
@ -202,6 +208,17 @@ export const TrafficDiagram = ({
</ListItemText>
</SquareMenuItem>
)}
{variant == "download" && (
<SquareMenuItem value={Source.encrypted_file}>
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("policy.encryptedFile")}
</ListItemText>
</SquareMenuItem>
)}
</DenseSelect>
<Box
sx={{

View File

@ -34,6 +34,7 @@ import DraggableDialog from "../../Dialogs/DraggableDialog.tsx";
import MoreVertical from "../../Icons/MoreVertical.tsx";
import { SquareMenuItem } from "../ContextMenu/ContextMenu.tsx";
import { FileManagerIndex } from "../FileManager.tsx";
import { EncryptionStatusText } from "../Sidebar/BasicInfo.tsx";
const VersionControl = () => {
const { t } = useTranslation();
@ -47,6 +48,7 @@ const VersionControl = () => {
const open = useAppSelector((state) => state.globalState.versionControlDialogOpen);
const target = useAppSelector((state) => state.globalState.versionControlDialogFile);
const highlight = useAppSelector((state) => state.globalState.versionControlHighlight);
const showEncryptionStatus = useAppSelector((state) => state.siteConfig?.explorer?.config?.show_encryption_status);
const onClose = useCallback(() => {
if (!loading) {
@ -216,6 +218,9 @@ const VersionControl = () => {
<NoWrapTableCell>{t("fileManager.size")}</NoWrapTableCell>
<NoWrapTableCell>{t("fileManager.createdBy")}</NoWrapTableCell>
<NoWrapTableCell>{t("application:fileManager.storagePolicy")}</NoWrapTableCell>
{showEncryptionStatus && (
<NoWrapTableCell>{t("application:fileManager.encryption")}</NoWrapTableCell>
)}
</TableRow>
</TableHead>
<TableBody>
@ -264,6 +269,17 @@ const VersionControl = () => {
/>
</TableCell>
<NoWrapTableCell>{e.storage_policy?.name}</NoWrapTableCell>
{showEncryptionStatus && (
<NoWrapTableCell>
<EncryptionStatusText
flexWrap={false}
status={{
status: e.encrypted_with ? "full" : "none",
cipher: e.encrypted_with ? [e.encrypted_with] : [],
}}
/>
</NoWrapTableCell>
)}
</TableRow>
))}
</TableBody>

View File

@ -1,14 +1,23 @@
import { Link, Skeleton, Typography } from "@mui/material";
import { Box, Link, Skeleton, Tooltip, Typography } from "@mui/material";
import dayjs from "dayjs";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { getFileInfo, sendPatchViewSync } from "../../../api/api.ts";
import { ExplorerView, FileResponse, FileType, FolderSummary, Metadata } from "../../../api/explorer.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import {
EncryptionCipher,
ExplorerView,
FileResponse,
FileType,
FolderSummary,
Metadata,
} from "../../../api/explorer.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import SessionManager from "../../../session/index.ts";
import { sizeToString } from "../../../util";
import CrUri from "../../../util/uri.ts";
import TimeBadge from "../../Common/TimeBadge.tsx";
import ShieldDismiss from "../../Icons/ShieldDismiss.tsx";
import ShieldLockFilled from "../../Icons/ShieldLockFilled.tsx";
import FileBadge from "../FileBadge.tsx";
import InfoRow from "./InfoRow.tsx";
@ -16,10 +25,95 @@ export interface BasicInfoProps {
target: FileResponse;
}
export interface EncryptionStatus {
status: "full" | "partial" | "none";
cipher: EncryptionCipher[];
}
export const cipherDisplayName = (cipher: EncryptionCipher): string => {
switch (cipher) {
case EncryptionCipher.aes256ctr:
return "AES-256-CTR";
default:
return cipher;
}
};
export const EncryptionStatusText = ({
status,
simplified = false,
flexWrap = true,
}: {
status: EncryptionStatus;
simplified?: boolean;
flexWrap?: boolean;
}) => {
const { t } = useTranslation();
const title = useMemo(() => {
switch (status.status) {
case "full":
return t("application:fileManager.fullEncryption", {
cipher: status.cipher.map(cipherDisplayName).join(", "),
});
case "partial":
return t("application:fileManager.partialEncryption");
}
return t("application:fileManager.noEncryption");
}, [status.status, t]);
const tooltipTitle = useMemo(() => {
if (simplified) {
return title;
}
return status.status === "partial" ? t("application:fileManager.partialEncryptionDes") : "";
}, [status.status, t, simplified]);
return (
<Tooltip title={tooltipTitle}>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, flexWrap: flexWrap ? "wrap" : "nowrap" }}>
{status.status === "full" ? (
<ShieldLockFilled
sx={{
width: "20px",
height: "20px",
color: (theme) => theme.palette.success.main,
}}
/>
) : status.status === "partial" ? (
<ShieldLockFilled
sx={{
width: "20px",
height: "20px",
color: (theme) => theme.palette.action.disabled,
}}
/>
) : (
<ShieldDismiss
sx={{
width: "20px",
height: "20px",
color: (theme) => theme.palette.action.disabled,
}}
/>
)}
{!simplified && (
<>
<Typography variant={"body2"} color={"text.secondary"}>
{title}
</Typography>
</>
)}
</Box>
</Tooltip>
);
};
const BasicInfo = ({ target }: BasicInfoProps) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const showEncryptionStatus = useAppSelector((state) => state.siteConfig?.explorer?.config?.show_encryption_status);
// null: not valid, undefined: not loaded, FolderSummary: loaded
const [folderSummary, setFolderSummary] = useState<FolderSummary | undefined | null>(null);
useEffect(() => {
@ -128,6 +222,27 @@ const BasicInfo = ({ target }: BasicInfoProps) => {
});
}, [folderSummary, t]);
const encryptionStatus = useMemo(() => {
if (target.extended_info) {
const status: EncryptionStatus = { status: "none", cipher: [] };
let encrypted = 0;
target.extended_info.entities?.forEach((entity) => {
if (entity.encrypted_with) {
encrypted++;
if (!status.cipher.includes(entity.encrypted_with)) {
status.cipher.push(entity.encrypted_with);
}
}
});
if (encrypted > 0) {
status.status = encrypted === target.extended_info.entities?.length ? "full" : "partial";
}
return <EncryptionStatusText status={status} />;
}
return <Skeleton variant={"text"} width={75} />;
}, [target.extended_info, t]);
const handleDeleteViewSetting = useCallback(() => {
dispatch(sendPatchViewSync({ uri: target.path }))
.then(() => {
@ -226,6 +341,9 @@ const BasicInfo = ({ target }: BasicInfoProps) => {
)
}
/>
{showEncryptionStatus && encryptionStatus && (
<InfoRow title={t("application:fileManager.encryption")} content={encryptionStatus} />
)}
</>
)}
<InfoRow

View File

@ -0,0 +1,9 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
export default function ShieldDismiss(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M12.45 2.15C14.992 4.057 17.587 5 20.25 5a.75.75 0 0 1 .75.75V11c0 5.001-2.958 8.676-8.725 10.948a.75.75 0 0 1-.55 0C5.958 19.676 3 16 3 11V5.75A.75.75 0 0 1 3.75 5c2.663 0 5.258-.943 7.8-2.85a.75.75 0 0 1 .9 0M12 3.678c-2.42 1.71-4.923 2.648-7.5 2.8V11c0 4.256 2.453 7.379 7.5 9.442c5.047-2.063 7.5-5.186 7.5-9.442V6.478c-2.577-.152-5.08-1.09-7.5-2.8M9.28 8.222l2.724 2.723l2.725-2.723a.75.75 0 0 1 .975-.073l.084.073a.75.75 0 0 1 .073.975l-.073.084l-2.724 2.723l2.724 2.725a.749.749 0 1 1-1.06 1.059l-2.724-2.724l-2.723 2.724a.75.75 0 0 1-.975.073l-.084-.073a.75.75 0 0 1-.073-.975l.073-.084l2.723-2.725l-2.723-2.723A.75.75 0 0 1 9.28 8.22" />
</SvgIcon>
);
}

View File

@ -0,0 +1,9 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
export default function ShieldLock(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M3 5.75A.75.75 0 0 1 3.75 5c2.663 0 5.258-.943 7.8-2.85a.75.75 0 0 1 .9 0C14.992 4.057 17.587 5 20.25 5a.75.75 0 0 1 .75.75V11c0 .181-.004.36-.012.539a3.499 3.499 0 0 0-1.488-.894V6.478c-2.577-.152-5.08-1.09-7.5-2.8c-2.42 1.71-4.923 2.648-7.5 2.8V11c0 4.256 2.453 7.379 7.5 9.442a21.01 21.01 0 0 0 1-.439V21.5c0 .05.001.098.004.146c-.238.104-.48.204-.73.302a.75.75 0 0 1-.549 0C5.958 19.676 3 16 3 11zM16 15v-1a2.5 2.5 0 0 1 5 0v1h.5a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5h-6a1.5 1.5 0 0 1-1.5-1.5v-5a1.5 1.5 0 0 1 1.5-1.5zm1.5-1v1h2v-1a1 1 0 1 0-2 0m2 5a1 1 0 1 0-2 0a1 1 0 0 0 2 0" />
</SvgIcon>
);
}

View File

@ -0,0 +1,9 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
export default function ShieldLockFilled(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M3 5.75A.75.75 0 0 1 3.75 5c2.663 0 5.258-.943 7.8-2.85a.75.75 0 0 1 .9 0C14.992 4.057 17.587 5 20.25 5a.75.75 0 0 1 .75.75V11c0 .181-.004.36-.012.539A3.5 3.5 0 0 0 15 14v.05a2.5 2.5 0 0 0-2 2.45v5c0 .05.001.098.004.146c-.238.104-.48.204-.73.302a.75.75 0 0 1-.549 0C5.958 19.676 3 16 3 11zM16 15v-1a2.5 2.5 0 0 1 5 0v1h.5a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5h-6a1.5 1.5 0 0 1-1.5-1.5v-5a1.5 1.5 0 0 1 1.5-1.5zm1.5-1v1h2v-1a1 1 0 1 0-2 0m2 5a1 1 0 1 0-2 0a1 1 0 0 0 2 0" />{" "}
</SvgIcon>
);
}

View File

@ -1,6 +1,6 @@
// 所有 Uploader 的基类
import axios, { CanceledError, CancelTokenSource } from "axios";
import { EncryptionAlgorithm, PolicyType } from "../../../../api/explorer.ts";
import { EncryptionCipher, PolicyType } from "../../../../api/explorer.ts";
import CrUri from "../../../../util/uri.ts";
import { createUploadSession, deleteUploadSession } from "../api";
import { UploaderError } from "../errors";
@ -148,7 +148,7 @@ export default abstract class Base {
mime_type: this.task.file.type,
entity_type: this.task.overwrite ? "version" : undefined,
encryption_supported:
this.task.policy.encryption && "crypto" in window ? [EncryptionAlgorithm.aes256ctr] : undefined,
this.task.policy.encryption && "crypto" in window ? [EncryptionCipher.aes256ctr] : undefined,
},
this.cancelToken.token,
);

View File

@ -1,4 +1,4 @@
import { EncryptMetadata, EncryptionAlgorithm } from "../../../../../api/explorer";
import { EncryptionCipher, EncryptMetadata } from "../../../../../api/explorer";
/**
* EncryptedBlob wraps a Blob and encrypts its stream on-the-fly using the provided encryption metadata.
@ -107,7 +107,7 @@ export class EncryptedBlob implements Blob {
const keyBytes = this.stringToUint8Array(this.metadata.key_plain_text);
switch (this.metadata.algorithm) {
case EncryptionAlgorithm.aes256ctr:
case EncryptionCipher.aes256ctr:
this.cryptoKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-CTR" }, false, ["encrypt"]);
break;
default: