From 8b91fca9291b58edd100949954039fc71524f97d Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 24 Oct 2025 15:04:51 +0800 Subject: [PATCH] feat(encryption): add UI and settings for file encryption --- public/locales/de-DE/application.json | 7 +- public/locales/de-DE/dashboard.json | 16 +- public/locales/en-US/application.json | 7 +- public/locales/en-US/dashboard.json | 16 +- public/locales/es-ES/application.json | 7 +- public/locales/es-ES/dashboard.json | 16 +- public/locales/fr-FR/application.json | 7 +- public/locales/fr-FR/dashboard.json | 16 +- public/locales/it-IT/application.json | 7 +- public/locales/it-IT/dashboard.json | 16 +- public/locales/ja-JP/application.json | 7 +- public/locales/ja-JP/dashboard.json | 16 +- public/locales/ko-KR/application.json | 7 +- public/locales/ko-KR/dashboard.json | 16 +- public/locales/pt-BR/application.json | 7 +- public/locales/pt-BR/dashboard.json | 16 +- public/locales/ru-RU/application.json | 7 +- public/locales/ru-RU/dashboard.json | 16 +- public/locales/zh-CN/application.json | 7 +- public/locales/zh-CN/dashboard.json | 16 +- public/locales/zh-TW/application.json | 7 +- public/locales/zh-TW/dashboard.json | 16 +- src/api/dashboard.ts | 12 +- src/api/explorer.ts | 7 +- src/api/site.ts | 1 + .../Admin/Entity/EntityDialog/EntityForm.tsx | 9 + src/component/Admin/Entity/EntityRow.tsx | 27 +- .../Admin/File/FileDialog/FileEntity.tsx | 14 + src/component/Admin/File/FileRow.tsx | 37 +- src/component/Admin/FileSystem/Filesystem.tsx | 5 +- src/component/Admin/FileSystem/Parameters.tsx | 610 ------------------ .../Parameters/AdvancedOptionsSection.tsx | 226 +++++++ .../Parameters/FileEncryptionSection.tsx | 106 +++ .../Parameters/FileSystemSection.tsx | 332 ++++++++++ .../FileSystem/Parameters/Parameters.tsx | 24 + .../Parameters/SearchQuerySection.tsx | 72 +++ .../FormSections/EncryptionSection.tsx | 49 ++ .../EditStoragePolicy/FormSections/index.ts | 1 + .../EditStoragePolicy/StoragePolicyForm.tsx | 2 + .../Admin/StoragePolicy/TrafficDiagram.tsx | 21 +- .../FileManager/Dialogs/VersionControl.tsx | 16 + .../FileManager/Sidebar/BasicInfo.tsx | 124 +++- src/component/Icons/ShieldDismiss.tsx | 9 + src/component/Icons/ShieldLock.tsx | 9 + src/component/Icons/ShieldLockFilled.tsx | 9 + src/component/Uploader/core/uploader/base.ts | 4 +- .../Uploader/core/uploader/encrypt/blob.ts | 4 +- 47 files changed, 1313 insertions(+), 670 deletions(-) delete mode 100644 src/component/Admin/FileSystem/Parameters.tsx create mode 100644 src/component/Admin/FileSystem/Parameters/AdvancedOptionsSection.tsx create mode 100644 src/component/Admin/FileSystem/Parameters/FileEncryptionSection.tsx create mode 100644 src/component/Admin/FileSystem/Parameters/FileSystemSection.tsx create mode 100644 src/component/Admin/FileSystem/Parameters/Parameters.tsx create mode 100644 src/component/Admin/FileSystem/Parameters/SearchQuerySection.tsx create mode 100644 src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/EncryptionSection.tsx create mode 100644 src/component/Icons/ShieldDismiss.tsx create mode 100644 src/component/Icons/ShieldLock.tsx create mode 100644 src/component/Icons/ShieldLockFilled.tsx diff --git a/public/locales/de-DE/application.json b/public/locales/de-DE/application.json index 67b9c76..d83a385 100644 --- a/public/locales/de-DE/application.json +++ b/public/locales/de-DE/application.json @@ -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", diff --git a/public/locales/de-DE/dashboard.json b/public/locales/de-DE/dashboard.json index efdf253..d550e44 100644 --- a/public/locales/de-DE/dashboard.json +++ b/public/locales/de-DE/dashboard.json @@ -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, um Datenverlust zu vermeiden.", + "masterEncryptionKeyVaultSetting": "Datenbank", + "masterEncryptionKeyVaultEnv": "Umgebungsvariable <0>CR_ENCRYPT_MASTER_KEY", + "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.", "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. Sie können die entsprechende Region in der Tabelle <1>OSS-Regionen und Endpunkte finden und die entsprechende <2>Regions-ID ausfüllen." + "ossRegionDes": "Geben Sie den Regionscode ein, in dem sich der Bucket befindet, z.B. <0>cn-hangzhou. Sie können die entsprechende Region in der Tabelle <1>OSS-Regionen und Endpunkte finden und die entsprechende <2>Regions-ID 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", diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index 594220c..99bf246 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -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", diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index 29fb1cf..7e578c6 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -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 to avoid data loss.", + "masterEncryptionKeyVaultSetting": "Database", + "masterEncryptionKeyVaultEnv": "Environment variable <0>CR_ENCRYPT_MASTER_KEY", + "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 .", "ks3EndpointPathStyle": "Select the format of the KS3 Endpoint address.", - "ossRegionDes": "Enter the region code of the storage bucket, e.g. <0>cn-hangzhou. You can find the corresponding region in the <1>OSS regions and endpoints table and fill in the corresponding <2>Region ID." + "ossRegionDes": "Enter the region code of the storage bucket, e.g. <0>cn-hangzhou. You can find the corresponding region in the <1>OSS regions and endpoints table and fill in the corresponding <2>Region ID.", + "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", diff --git a/public/locales/es-ES/application.json b/public/locales/es-ES/application.json index b3508a3..f42bdfa 100644 --- a/public/locales/es-ES/application.json +++ b/public/locales/es-ES/application.json @@ -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", diff --git a/public/locales/es-ES/dashboard.json b/public/locales/es-ES/dashboard.json index 05eb143..144fe3f 100644 --- a/public/locales/es-ES/dashboard.json +++ b/public/locales/es-ES/dashboard.json @@ -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 para evitar la pérdida de datos.", + "masterEncryptionKeyVaultSetting": "Base de datos", + "masterEncryptionKeyVaultEnv": "Variable de entorno <0>CR_ENCRYPT_MASTER_KEY", + "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 .", "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. Puedes encontrar la región correspondiente en la tabla <1>Regiones y endpoints de OSS y completar el <2>ID de región correspondiente." + "ossRegionDes": "Ingresa el código de región donde se encuentra el bucket, ej. <0>cn-hangzhou. Puedes encontrar la región correspondiente en la tabla <1>Regiones y endpoints de OSS y completar el <2>ID de región 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", diff --git a/public/locales/fr-FR/application.json b/public/locales/fr-FR/application.json index ab63dad..b2d1091 100644 --- a/public/locales/fr-FR/application.json +++ b/public/locales/fr-FR/application.json @@ -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", diff --git a/public/locales/fr-FR/dashboard.json b/public/locales/fr-FR/dashboard.json index 9aa9f8b..3557176 100644 --- a/public/locales/fr-FR/dashboard.json +++ b/public/locales/fr-FR/dashboard.json @@ -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 pour éviter la perte de données.", + "masterEncryptionKeyVaultSetting": "Base de données", + "masterEncryptionKeyVaultEnv": "Variable d'environnement <0>CR_ENCRYPT_MASTER_KEY", + "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 .", "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. Vous pouvez trouver la région correspondante dans le tableau <1>Régions et points de terminaison OSS et remplir l'<2>ID de région correspondant." + "ossRegionDes": "Entrez le code de région où se trouve le bucket, par ex. <0>cn-hangzhou. Vous pouvez trouver la région correspondante dans le tableau <1>Régions et points de terminaison OSS et remplir l'<2>ID de région 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", diff --git a/public/locales/it-IT/application.json b/public/locales/it-IT/application.json index 8a62e92..f386020 100644 --- a/public/locales/it-IT/application.json +++ b/public/locales/it-IT/application.json @@ -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", diff --git a/public/locales/it-IT/dashboard.json b/public/locales/it-IT/dashboard.json index aeffe25..39391f2 100644 --- a/public/locales/it-IT/dashboard.json +++ b/public/locales/it-IT/dashboard.json @@ -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 per evitare la perdita di dati.", + "masterEncryptionKeyVaultSetting": "Database", + "masterEncryptionKeyVaultEnv": "Variabile d'ambiente <0>CR_ENCRYPT_MASTER_KEY", + "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.", "ks3EndpointPathStyle": "Seleziona il formato dell'indirizzo KS3 Endpoint.", - "ossRegionDes": "Inserisci il codice regione dove si trova il bucket, ad es. <0>cn-hangzhou. Puoi trovare la regione corrispondente nella tabella <1>Regioni ed endpoint OSS e inserire il <2>ID regione corrispondente." + "ossRegionDes": "Inserisci il codice regione dove si trova il bucket, ad es. <0>cn-hangzhou. Puoi trovare la regione corrispondente nella tabella <1>Regioni ed endpoint OSS e inserire il <2>ID regione 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", diff --git a/public/locales/ja-JP/application.json b/public/locales/ja-JP/application.json index 16e4785..be4cbc6 100644 --- a/public/locales/ja-JP/application.json +++ b/public/locales/ja-JP/application.json @@ -401,7 +401,12 @@ "viewSetting": "ビュー設定", "saved": "保存", "notSet": "未設定", - "deleteViewSetting": "ビュー設定を削除" + "deleteViewSetting": "ビュー設定を削除", + "encryption": "暗号化", + "fullEncryption": "{{cipher}} で暗号化", + "partialEncryption": "部分的に暗号化", + "partialEncryptionDes": "このファイルの一部のバージョンまたは他のデータは暗号化されていません。", + "noEncryption": "暗号化されていません" }, "modals": { "includePasswordInShareLink": "共有リンクにパスワードを含める", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index 7ed65c1..273ddb3 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -716,7 +716,15 @@ "blobUrlCache": "Blob URLキャッシュ", "clearBlobUrlCache": "Blob URLキャッシュのクリア", "clearBlobUrlCacheDes": "キャッシュヒット率を高めるため、CloudreveはBlob URLをキャッシュして再利用します。CDNアドレスなどの設定が変更された場合は、キャッシュをクリアしてください。", - "cacheCleared": "キャッシュをクリアしました" + "cacheCleared": "キャッシュをクリアしました", + "masterEncryptionKeyVault": "マスター暗号化キーの保存方法", + "masterEncryptionKeyVaultDes": "マスター暗号化キーの保存方法を選択します。再起動後に有効になります。保存方法またはキーを変更する必要がある場合は、データ損失を避けるために<0>ドキュメントの手順に厳密に従ってください。", + "masterEncryptionKeyVaultSetting": "データベース", + "masterEncryptionKeyVaultEnv": "環境変数 <0>CR_ENCRYPT_MASTER_KEY", + "masterEncryptionKeyVaultFile": "ファイル", + "masterEncryptionKeyVaultFilePath": "キー保存パス", + "showEncryptionStatus": "暗号化状態を表示", + "showEncryptionStatusDes": "有効にすると、ユーザーはファイル詳細でファイルの暗号化状態を確認できます。" }, "giftCodes": { "giftCodesSettings": "ギフトコード", @@ -1034,7 +1042,11 @@ "sharePointUrlDes": "SharePointサイトのURLを入力してください。フォーカスが外れると、システムが自動的に正しいドライブ識別子に変換します。", "ks3selectRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>BEIJING)。", "ks3EndpointPathStyle": "パス形式エンドポイントの強制使用を選択してください。", - "ossRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>cn-hangzhou)。<1>OSSリージョンとエンドポイントの表で対応するリージョンを見つけ、対応する<2>リージョンIDを入力できます。" + "ossRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>cn-hangzhou)。<1>OSSリージョンとエンドポイントの表で対応するリージョンを見つけ、対応する<2>リージョンIDを入力できます。", + "fileEncryption": "ファイル暗号化", + "enableFileEncryption": "ファイル暗号化を有効にする", + "enableFileEncryptionDes": "有効にすると、ファイルのBlobが暗号化されて保存されます。この設定の変更は新しく追加されたファイルにのみ有効です。", + "encryptedFile": "暗号化されたファイル" }, "node": { "slave": "スレーブ", diff --git a/public/locales/ko-KR/application.json b/public/locales/ko-KR/application.json index 72de585..02de94d 100644 --- a/public/locales/ko-KR/application.json +++ b/public/locales/ko-KR/application.json @@ -436,7 +436,12 @@ "viewSetting": "보기 설정", "saved": "저장됨", "notSet": "설정되지 않음", - "deleteViewSetting": "보기 설정 삭제" + "deleteViewSetting": "보기 설정 삭제", + "encryption": "암호화", + "fullEncryption": "{{cipher}}로 암호화됨", + "partialEncryption": "부분 암호화", + "partialEncryptionDes": "이 파일의 일부 버전 또는 기타 데이터가 암호화되지 않았습니다.", + "noEncryption": "암호화되지 않음" }, "modals": { "includePasswordInShareLink": "공유 링크에 비밀번호 포함", diff --git a/public/locales/ko-KR/dashboard.json b/public/locales/ko-KR/dashboard.json index 19b4f2c..87edb9f 100644 --- a/public/locales/ko-KR/dashboard.json +++ b/public/locales/ko-KR/dashboard.json @@ -716,7 +716,15 @@ "blobUrlCache": "Blob URL 캐시", "clearBlobUrlCache": "Blob URL 캐시 지우기", "clearBlobUrlCacheDes": "캐시 적중률을 높이기 위해 Cloudreve는 Blob URL을 캐시하고 재사용합니다. CDN 주소 등의 설정이 변경되면 캐시를 지워 주세요.", - "cacheCleared": "캐시가 지워졌습니다." + "cacheCleared": "캐시가 지워졌습니다.", + "masterEncryptionKeyVault": "마스터 암호화 키 저장 방식", + "masterEncryptionKeyVaultDes": "마스터 암호화 키의 저장 방식을 선택하며, 재시작 후 적용됩니다. 저장 방식이나 키를 변경해야 하는 경우 데이터 손실을 방지하기 위해 <0>문서의 단계를 엄격히 따르십시오.", + "masterEncryptionKeyVaultSetting": "데이터베이스", + "masterEncryptionKeyVaultEnv": "환경 변수 <0>CR_ENCRYPT_MASTER_KEY", + "masterEncryptionKeyVaultFile": "파일", + "masterEncryptionKeyVaultFilePath": "키 저장 경로", + "showEncryptionStatus": "암호화 상태 표시", + "showEncryptionStatusDes": "활성화하면 사용자가 파일 상세 정보에서 파일의 암호화 상태를 볼 수 있습니다." }, "giftCodes": { "giftCodesSettings": "기프트 코드", @@ -1034,7 +1042,11 @@ "sharePointUrlDes": "SharePoint 사이트 URL을 입력하세요. 포커스를 잃으면 시스템이 자동으로 올바른 드라이브 식별자로 변환합니다.", "ks3selectRegionDes": "스토리지 버킷이 위치한 지역 코드를 입력하세요. 예: <0>BEIJING.", "ks3EndpointPathStyle": "경로 형식 Endpoint를 강제로 사용할지 선택하세요.", - "ossRegionDes": "버킷이 위치한 지역 코드를 입력하세요. 예: <0>cn-hangzhou. <1>OSS 지역 및 엔드포인트 표에서 해당 지역을 찾아 해당하는 <2>지역 ID를 입력할 수 있습니다." + "ossRegionDes": "버킷이 위치한 지역 코드를 입력하세요. 예: <0>cn-hangzhou. <1>OSS 지역 및 엔드포인트 표에서 해당 지역을 찾아 해당하는 <2>지역 ID를 입력할 수 있습니다.", + "fileEncryption": "파일 암호화", + "enableFileEncryption": "파일 암호화 활성화", + "enableFileEncryptionDes": "활성화하면 파일 Blob이 암호화되어 저장됩니다. 이 설정을 변경하면 새로 추가되는 파일에만 적용됩니다.", + "encryptedFile": "암호화된 파일" }, "node": { "slave": "슬레이브", diff --git a/public/locales/pt-BR/application.json b/public/locales/pt-BR/application.json index d826853..8fbc7c4 100644 --- a/public/locales/pt-BR/application.json +++ b/public/locales/pt-BR/application.json @@ -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", diff --git a/public/locales/pt-BR/dashboard.json b/public/locales/pt-BR/dashboard.json index 5067b53..8cc7028 100644 --- a/public/locales/pt-BR/dashboard.json +++ b/public/locales/pt-BR/dashboard.json @@ -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 para evitar perda de dados.", + "masterEncryptionKeyVaultSetting": "Banco de dados", + "masterEncryptionKeyVaultEnv": "Variável de ambiente <0>CR_ENCRYPT_MASTER_KEY", + "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 .", "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. Você pode encontrar a região correspondente na tabela <1>Regiões e endpoints do OSS e preencher o <2>ID da região correspondente." + "ossRegionDes": "Digite o código da região onde está localizado o bucket, ex. <0>cn-hangzhou. Você pode encontrar a região correspondente na tabela <1>Regiões e endpoints do OSS e preencher o <2>ID da região 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", diff --git a/public/locales/ru-RU/application.json b/public/locales/ru-RU/application.json index 787c00b..db44376 100644 --- a/public/locales/ru-RU/application.json +++ b/public/locales/ru-RU/application.json @@ -436,7 +436,12 @@ "viewSetting": "Настройки просмотра", "saved": "Сохранено", "notSet": "Не установлено", - "deleteViewSetting": "Удалить настройки просмотра" + "deleteViewSetting": "Удалить настройки просмотра", + "encryption": "Шифрование", + "fullEncryption": "Зашифровано с помощью {{cipher}}", + "partialEncryption": "Частично зашифровано", + "partialEncryptionDes": "Некоторые версии или другие данные этого файла не зашифрованы.", + "noEncryption": "Не зашифровано" }, "modals": { "includePasswordInShareLink": "Включить пароль в ссылку на публикацию", diff --git a/public/locales/ru-RU/dashboard.json b/public/locales/ru-RU/dashboard.json index d024c86..64f8d78 100644 --- a/public/locales/ru-RU/dashboard.json +++ b/public/locales/ru-RU/dashboard.json @@ -717,7 +717,15 @@ "blobUrlCache": "Кэш Blob URL", "clearBlobUrlCache": "Очистить кэш Blob URL", "clearBlobUrlCacheDes": "Для увеличения частоты попаданий в кэш Cloudreve кэширует и повторно использует Blob URL. При изменении настроек, таких как адрес CDN, пожалуйста, очистите кэш.", - "cacheCleared": "Кэш очищен" + "cacheCleared": "Кэш очищен", + "masterEncryptionKeyVault": "Хранилище главного ключа шифрования", + "masterEncryptionKeyVaultDes": "Выберите способ хранения главного ключа шифрования, вступает в силу после перезапуска. Если вам нужно изменить способ хранения или ключ, строго следуйте шагам в <0>документации, чтобы избежать потери данных.", + "masterEncryptionKeyVaultSetting": "База данных", + "masterEncryptionKeyVaultEnv": "Переменная окружения <0>CR_ENCRYPT_MASTER_KEY", + "masterEncryptionKeyVaultFile": "Файл", + "masterEncryptionKeyVaultFilePath": "Путь хранения ключа", + "showEncryptionStatus": "Показывать статус шифрования", + "showEncryptionStatusDes": "При включении пользователи могут просматривать статус шифрования файлов в детальной информации о файле." }, "giftCodes": { "giftCodesSettings": "Подарочные коды", @@ -1035,7 +1043,11 @@ "sharePointUrlDes": "Введите URL сайта SharePoint. После потери фокуса система автоматически преобразует его в правильный идентификатор диска.", "ks3selectRegionDes": "Введите код региона, где находится корзина, например <0>BEIJING.", "ks3EndpointPathStyle": "Выберите, принудительно ли использовать Endpoint в формате пути.", - "ossRegionDes": "Введите код региона, где находится корзина, например <0>cn-hangzhou. Вы можете найти соответствующий регион в таблице <1>Регионы и конечные точки OSS и заполнить соответствующий <2>ID региона." + "ossRegionDes": "Введите код региона, где находится корзина, например <0>cn-hangzhou. Вы можете найти соответствующий регион в таблице <1>Регионы и конечные точки OSS и заполнить соответствующий <2>ID региона.", + "fileEncryption": "Шифрование файлов", + "enableFileEncryption": "Включить шифрование файлов", + "enableFileEncryptionDes": "При включении блобы файлов будут храниться в зашифрованном виде. Изменение этого параметра влияет только на вновь добавляемые файлы.", + "encryptedFile": "Зашифрованные файлы" }, "node": { "slave": "Подчиненный узел", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index b11521f..e988324 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -401,7 +401,12 @@ "viewSetting": "视图设置", "saved": "已保存", "notSet": "未设置", - "deleteViewSetting": "删除视图设置" + "deleteViewSetting": "删除视图设置", + "encryption": "加密", + "fullEncryption": "使用 {{cipher}} 加密", + "partialEncryption": "部分加密", + "partialEncryptionDes": "此文件的某些版本或其他数据未被加密。", + "noEncryption": "未加密" }, "modals": { "includePasswordInShareLink": "在链接中包含密码", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index f8cfe32..37641de 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -716,7 +716,15 @@ "blobUrlCache": "Blob URL 缓存", "clearBlobUrlCache": "清除 Blob URL 缓存", "clearBlobUrlCacheDes": "为了增加缓存命中率,Cloudreve 会缓存并复用 Blob URL。当 CDN 地址等设置发生变更时,请清除缓存。", - "cacheCleared": "缓存已清除" + "cacheCleared": "缓存已清除", + "masterEncryptionKeyVault": "主加密密钥存储方式", + "masterEncryptionKeyVaultDes": "选择主加密密钥的存储方式,重启后生效。如果需要更换存储方式或密钥,请严格遵循<0>文档中的步骤,避免数据丢失。", + "masterEncryptionKeyVaultSetting": "数据库", + "masterEncryptionKeyVaultEnv": "环境变量 <0>CR_ENCRYPT_MASTER_KEY", + "masterEncryptionKeyVaultFile": "文件", + "masterEncryptionKeyVaultFilePath": "密钥存储路径", + "showEncryptionStatus": "显示加密状态", + "showEncryptionStatusDes": "开启后,用户在文件详情中可以查看文件的加密状态。" }, "giftCodes": { "giftCodesSettings": "礼品码", @@ -1034,7 +1042,11 @@ "sharePointUrlDes": "输入 SharePoint 站点 URL。失去焦点后,系统将自动转换为正确的驱动器标识。", "ks3selectRegionDes": "输入存储桶所在的区域代码,如 <0>BEIJING。", "ks3EndpointPathStyle": "选择是否强制使用路径格式 Endpoint。", - "ossRegionDes": "输入存储桶所在的区域代码,如 <0>cn-hangzhou。你可以在 <1>OSS地域和访问域名 的表格中找到对应地域,并填写对应的 <2>地域ID。" + "ossRegionDes": "输入存储桶所在的区域代码,如 <0>cn-hangzhou。你可以在 <1>OSS地域和访问域名 的表格中找到对应地域,并填写对应的 <2>地域ID。", + "fileEncryption": "文件加密", + "enableFileEncryption": "启用文件加密", + "enableFileEncryptionDes": "开启后,文件 Blob 会被加密存储。更改此设置只对新增的文件有效。", + "encryptedFile": "加密的文件" }, "node": { "slave": "从机", diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 1aac3d0..9400c03 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -401,7 +401,12 @@ "viewSetting": "檢視設定", "saved": "已保存", "notSet": "未設定", - "deleteViewSetting": "刪除檢視設定" + "deleteViewSetting": "刪除檢視設定", + "encryption": "加密", + "fullEncryption": "使用 {{cipher}} 加密", + "partialEncryption": "部分加密", + "partialEncryptionDes": "此檔案的某些版本或其他資料未被加密。", + "noEncryption": "未加密" }, "modals": { "includePasswordInShareLink": "在連結中包含密碼", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index c2069dd..c1a4f33 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -716,7 +716,15 @@ "blobUrlCache": "Blob URL 快取", "clearBlobUrlCache": "清除 Blob URL 快取", "clearBlobUrlCacheDes": "為了增加快取命中率,Cloudreve 會快取並復用 Blob URL。當 CDN 地址等設定發生變更時,請清除快取。", - "cacheCleared": "快取已清除" + "cacheCleared": "快取已清除", + "masterEncryptionKeyVault": "主加密金鑰儲存方式", + "masterEncryptionKeyVaultDes": "選擇主加密金鑰的儲存方式,重啟後生效。如果需要更換儲存方式或金鑰,請嚴格遵循<0>文件中的步驟,避免資料遺失。", + "masterEncryptionKeyVaultSetting": "資料庫", + "masterEncryptionKeyVaultEnv": "環境變數 <0>CR_ENCRYPT_MASTER_KEY", + "masterEncryptionKeyVaultFile": "檔案", + "masterEncryptionKeyVaultFilePath": "金鑰儲存路徑", + "showEncryptionStatus": "顯示加密狀態", + "showEncryptionStatusDes": "開啟後,使用者在檔案詳情中可以查看檔案的加密狀態。" }, "giftCodes": { "giftCodesSettings": "禮品碼", @@ -1034,7 +1042,11 @@ "sharePointUrlDes": "輸入 SharePoint 站點 URL。失去焦點後,系統將自動轉換為正確的驅動器標識。", "ks3selectRegionDes": "輸入儲存桶所在的區域程式碼,如 <0>BEIJING。", "ks3EndpointPathStyle": "選擇是否強制使用路徑格式 Endpoint。", - "ossRegionDes": "輸入儲存桶所在的區域代碼,如 <0>cn-hangzhou。你可以在 <1>OSS地域和訪問域名 的表格中找到對應地域,並填寫對應的 <2>地域ID。" + "ossRegionDes": "輸入儲存桶所在的區域代碼,如 <0>cn-hangzhou。你可以在 <1>OSS地域和訪問域名 的表格中找到對應地域,並填寫對應的 <2>地域ID。", + "fileEncryption": "檔案加密", + "enableFileEncryption": "啟用檔案加密", + "enableFileEncryptionDes": "開啟後,檔案 Blob 會被加密儲存。更改此設定只對新增的檔案有效。", + "encryptedFile": "加密的檔案" }, "node": { "slave": "從機", diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts index bed893f..e6b1144 100644 --- a/src/api/dashboard.ts +++ b/src/api/dashboard.ts @@ -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; } +export interface EntityProps { + encrypt_metadata?: EncryptMetadata; +} + +export interface EncryptMetadata { + algorithm: EncryptionCipher; +} + export interface Metadata extends CommonMixin { name?: string; value?: string; diff --git a/src/api/explorer.ts b/src/api/explorer.ts index f1204af..147700c 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -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; } diff --git a/src/api/site.ts b/src/api/site.ts index 829f51d..cbc0804 100644 --- a/src/api/site.ts +++ b/src/api/site.ts @@ -46,6 +46,7 @@ export interface SiteConfig { custom_nav_items?: CustomNavItem[]; custom_html?: CustomHTML; thumb_exts?: string[]; + show_encryption_status?: boolean; } export interface CaptchaResponse { diff --git a/src/component/Admin/Entity/EntityDialog/EntityForm.tsx b/src/component/Admin/Entity/EntityDialog/EntityForm.tsx index 39b2202..3e4592d 100644 --- a/src/component/Admin/Entity/EntityDialog/EntityForm.tsx +++ b/src/component/Admin/Entity/EntityDialog/EntityForm.tsx @@ -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 }) => { + + + diff --git a/src/component/Admin/Entity/EntityRow.tsx b/src/component/Admin/Entity/EntityRow.tsx index 2e84079..943eb51 100644 --- a/src/component/Admin/Entity/EntityRow.tsx +++ b/src/component/Admin/Entity/EntityRow.tsx @@ -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) => { 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 = ({ {!entity?.reference_count && } + {entity?.props?.encrypt_metadata?.algorithm && ( + + theme.palette.success.main, + }} + /> + + )} diff --git a/src/component/Admin/File/FileDialog/FileEntity.tsx b/src/component/Admin/File/FileDialog/FileEntity.tsx index 677ac40..3a4a142 100644 --- a/src/component/Admin/File/FileDialog/FileEntity.tsx +++ b/src/component/Admin/File/FileDialog/FileEntity.tsx @@ -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 = () => { {t("file.size")} {t("file.storagePolicy")} {t("file.source")} + {t("application:fileManager.encryption")} {t("file.createdAt")} {t("file.creator")} @@ -91,6 +93,18 @@ const FileEntity = () => { {option.source ?? ""} + + + diff --git a/src/component/Admin/File/FileRow.tsx b/src/component/Admin/File/FileRow.tsx index 8104798..ab86b62 100644 --- a/src/component/Admin/File/FileRow.tsx +++ b/src/component/Admin/File/FileRow.tsx @@ -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) => { 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 ( @@ -203,6 +221,9 @@ const FileRow = ({ )} + {encryptionStatus && encryptionStatus.status !== "none" && ( + + )} diff --git a/src/component/Admin/FileSystem/Filesystem.tsx b/src/component/Admin/FileSystem/Filesystem.tsx index e4d1a16..3fe0b23 100644 --- a/src/component/Admin/FileSystem/Filesystem.tsx +++ b/src/component/Admin/FileSystem/Filesystem.tsx @@ -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", ]} > diff --git a/src/component/Admin/FileSystem/Parameters.tsx b/src/component/Admin/FileSystem/Parameters.tsx deleted file mode 100644 index d0984e3..0000000 --- a/src/component/Admin/FileSystem/Parameters.tsx +++ /dev/null @@ -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) => { - setSettings({ - mime_mapping: e.target.value, - }); - }, []); - - return ( - e.preventDefault()}> - - - - {t("nav.fileSystem")} - - - - - - setSettings({ - maxEditSize: e.toString(), - }) - } - /> - {t("settings.textEditMaxSizeDes")} - - - - - - setSettings({ - cron_trash_bin_collect: e.target.value, - }) - } - required - /> - - ]} - /> - - - - - - - setSettings({ - cron_entity_collect: e.target.value, - }) - } - required - /> - - ]} - /> - - - - - - - setSettings({ - public_resource_maxage: e.target.value, - }) - } - required - /> - {t("settings.publicResourceMaxAgeDes")} - - - - - ( - - {v == "0" ? t("settings.offsetPagination") : t("settings.cursorPagination")} - - )} - 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} - > - - - - {t("settings.offsetPagination")} - - - {t("settings.offsetPaginationDes")} - - - - - - - {t("settings.cursorPagination")} - - - {t("settings.cursorPaginationDes")} - - - - - {t("settings.defaultPaginationDes")} - - - - - - setSettings({ - max_page_size: e.target.value, - }) - } - required - /> - {t("settings.maxPageSizeDes")} - - - - - - setSettings({ - max_batched_file: e.target.value, - }) - } - required - /> - {t("settings.maxBatchSizeDes")} - - - - - - setSettings({ - max_recursive_searched_folder: e.target.value, - }) - } - required - /> - {t("settings.maxRecursiveSearchDes")} - - - - - - setSettings({ - map_provider: e.target.value as string, - }) - } - value={values.map_provider} - > - - - {t("settings.mapGoogle")} - - - - - {t("settings.mapOpenStreetMap")} - - - - - {t("settings.mapboxMap")} - - - - {t("settings.mapProviderDes")} - - - - - - setSettings({ map_mapbox_ak: e.target.value })} - /> - - ]} - /> - - - - - - - - - setSettings({ - map_google_tile_type: e.target.value as string, - }) - } - value={values.map_google_tile_type} - > - - - {t("settings.tileTypeTerrain")} - - - - - {t("settings.tileTypeSatellite")} - - - - - {t("settings.tileTypeGeneral")} - - - - {t("settings.tileTypeDes")} - - - - - - - {t("settings.mimeMappingDes")} - - - - - - - {t("settings.searchQuery")} - - - - - setSettings({ - explorer_category_image_query: e.target.value, - }) - } - required - /> - - - - setSettings({ - explorer_category_video_query: e.target.value, - }) - } - required - /> - - - - setSettings({ - explorer_category_audio_query: e.target.value, - }) - } - required - /> - - - - setSettings({ - explorer_category_document_query: e.target.value, - }) - } - required - /> - - - - - - {t("settings.advanceOptions")} - - - - - setSettings({ - archive_timeout: e.target.value, - }) - } - required - /> - - - - setSettings({ - upload_session_timeout: e.target.value, - }) - } - required - /> - {t("settings.uploadSessionDes")} - - - - setSettings({ - slave_api_timeout: e.target.value, - }) - } - required - /> - {t("settings.slaveAPIExpirationDes")} - - - - setSettings({ - folder_props_timeout: e.target.value, - }) - } - required - /> - {t("settings.folderPropsTimeoutDes")} - - - - setSettings({ - chunk_retries: e.target.value, - }) - } - required - /> - {t("settings.failedChunkRetryDes")} - - - - - setSettings({ - use_temp_chunk_buffer: e.target.checked ? "1" : "0", - }) - } - /> - } - label={t("settings.cacheChunks")} - /> - {t("settings.cacheChunksDes")} - - - - - setSettings({ - max_parallel_transfer: e.target.value, - }) - } - required - /> - {t("settings.transitParallelNumDes")} - - - - setSettings({ - cron_oauth_cred_refresh: e.target.value, - }) - } - /> - - ]} - /> - - - - - setSettings({ - viewer_session_timeout: e.target.value, - }) - } - required - /> - {t("settings.wopiSessionTimeoutDes")} - - - - setSettings({ - entity_url_default_ttl: e.target.value, - }) - } - required - /> - {t("settings.fileBlobTimeoutDes")} - - - - setSettings({ - entity_url_cache_margin: e.target.value, - }) - } - required - /> - {t("settings.fileBlobMarginDes")} - - - - - } - variant="contained" - loading={loading} - color="primary" - onClick={clearBlobUrlCache} - > - {t("settings.clearBlobUrlCache")} - - - {t("settings.clearBlobUrlCacheDes")} - - - - - - - ); -}; - -export default Parameters; diff --git a/src/component/Admin/FileSystem/Parameters/AdvancedOptionsSection.tsx b/src/component/Admin/FileSystem/Parameters/AdvancedOptionsSection.tsx new file mode 100644 index 0000000..685b0ab --- /dev/null +++ b/src/component/Admin/FileSystem/Parameters/AdvancedOptionsSection.tsx @@ -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 ( + + + {t("settings.advanceOptions")} + + + + + setSettings({ + archive_timeout: e.target.value, + }) + } + required + /> + + + + setSettings({ + upload_session_timeout: e.target.value, + }) + } + required + /> + {t("settings.uploadSessionDes")} + + + + setSettings({ + slave_api_timeout: e.target.value, + }) + } + required + /> + {t("settings.slaveAPIExpirationDes")} + + + + setSettings({ + folder_props_timeout: e.target.value, + }) + } + required + /> + {t("settings.folderPropsTimeoutDes")} + + + + setSettings({ + chunk_retries: e.target.value, + }) + } + required + /> + {t("settings.failedChunkRetryDes")} + + + + + setSettings({ + use_temp_chunk_buffer: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.cacheChunks")} + /> + {t("settings.cacheChunksDes")} + + + + + setSettings({ + max_parallel_transfer: e.target.value, + }) + } + required + /> + {t("settings.transitParallelNumDes")} + + + + setSettings({ + cron_oauth_cred_refresh: e.target.value, + }) + } + /> + + ]} + /> + + + + + setSettings({ + viewer_session_timeout: e.target.value, + }) + } + required + /> + {t("settings.wopiSessionTimeoutDes")} + + + + setSettings({ + entity_url_default_ttl: e.target.value, + }) + } + required + /> + {t("settings.fileBlobTimeoutDes")} + + + + setSettings({ + entity_url_cache_margin: e.target.value, + }) + } + required + /> + {t("settings.fileBlobMarginDes")} + + + + + } + variant="contained" + loading={loading} + color="primary" + onClick={clearBlobUrlCache} + > + {t("settings.clearBlobUrlCache")} + + + {t("settings.clearBlobUrlCacheDes")} + + + + + ); +}; + +export default AdvancedOptionsSection; diff --git a/src/component/Admin/FileSystem/Parameters/FileEncryptionSection.tsx b/src/component/Admin/FileSystem/Parameters/FileEncryptionSection.tsx new file mode 100644 index 0000000..aa9f2ca --- /dev/null +++ b/src/component/Admin/FileSystem/Parameters/FileEncryptionSection.tsx @@ -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 ( + + + {t("policy.fileEncryption")} + + + + + + setSettings({ + encrypt_master_key_vault: e.target.value as string, + }) + } + value={values.encrypt_master_key_vault} + > + + + {t("settings.masterEncryptionKeyVaultSetting")} + + + + + {t("settings.masterEncryptionKeyVaultFile")} + + + + + {]} />} + + + + + , + ]} + /> + + + + + + + setSettings({ encrypt_master_key_file: e.target.value })} + /> + + + + + + setSettings({ show_encryption_status: e.target.checked ? "1" : "0" })} + /> + } + label={t("settings.showEncryptionStatus")} + /> + + + + + + + + ); +}; + +export default FileEncryptionSection; diff --git a/src/component/Admin/FileSystem/Parameters/FileSystemSection.tsx b/src/component/Admin/FileSystem/Parameters/FileSystemSection.tsx new file mode 100644 index 0000000..9178821 --- /dev/null +++ b/src/component/Admin/FileSystem/Parameters/FileSystemSection.tsx @@ -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) => { + setSettings({ + mime_mapping: e.target.value, + }); + }, []); + + return ( + + + {t("nav.fileSystem")} + + + + + + setSettings({ + maxEditSize: e.toString(), + }) + } + /> + {t("settings.textEditMaxSizeDes")} + + + + + + setSettings({ + cron_trash_bin_collect: e.target.value, + }) + } + required + /> + + ]} + /> + + + + + + + setSettings({ + cron_entity_collect: e.target.value, + }) + } + required + /> + + ]} + /> + + + + + + + setSettings({ + public_resource_maxage: e.target.value, + }) + } + required + /> + {t("settings.publicResourceMaxAgeDes")} + + + + + ( + + {v == "0" ? t("settings.offsetPagination") : t("settings.cursorPagination")} + + )} + 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} + > + + + + {t("settings.offsetPagination")} + + + {t("settings.offsetPaginationDes")} + + + + + + + {t("settings.cursorPagination")} + + + {t("settings.cursorPaginationDes")} + + + + + {t("settings.defaultPaginationDes")} + + + + + + setSettings({ + max_page_size: e.target.value, + }) + } + required + /> + {t("settings.maxPageSizeDes")} + + + + + + setSettings({ + max_batched_file: e.target.value, + }) + } + required + /> + {t("settings.maxBatchSizeDes")} + + + + + + setSettings({ + max_recursive_searched_folder: e.target.value, + }) + } + required + /> + {t("settings.maxRecursiveSearchDes")} + + + + + + setSettings({ + map_provider: e.target.value as string, + }) + } + value={values.map_provider} + > + + + {t("settings.mapGoogle")} + + + + + {t("settings.mapOpenStreetMap")} + + + + + {t("settings.mapboxMap")} + + + + {t("settings.mapProviderDes")} + + + + + + setSettings({ map_mapbox_ak: e.target.value })} + /> + + ]} + /> + + + + + + + + + setSettings({ + map_google_tile_type: e.target.value as string, + }) + } + value={values.map_google_tile_type} + > + + + {t("settings.tileTypeTerrain")} + + + + + {t("settings.tileTypeSatellite")} + + + + + {t("settings.tileTypeGeneral")} + + + + {t("settings.tileTypeDes")} + + + + + + + {t("settings.mimeMappingDes")} + + + + + ); +}; + +export default FileSystemSection; diff --git a/src/component/Admin/FileSystem/Parameters/Parameters.tsx b/src/component/Admin/FileSystem/Parameters/Parameters.tsx new file mode 100644 index 0000000..03f5077 --- /dev/null +++ b/src/component/Admin/FileSystem/Parameters/Parameters.tsx @@ -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 ( + e.preventDefault()}> + + + + + + + + ); +}; + +export default Parameters; diff --git a/src/component/Admin/FileSystem/Parameters/SearchQuerySection.tsx b/src/component/Admin/FileSystem/Parameters/SearchQuerySection.tsx new file mode 100644 index 0000000..d6bf763 --- /dev/null +++ b/src/component/Admin/FileSystem/Parameters/SearchQuerySection.tsx @@ -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 ( + + + {t("settings.searchQuery")} + + + + + setSettings({ + explorer_category_image_query: e.target.value, + }) + } + required + /> + + + + setSettings({ + explorer_category_video_query: e.target.value, + }) + } + required + /> + + + + setSettings({ + explorer_category_audio_query: e.target.value, + }) + } + required + /> + + + + setSettings({ + explorer_category_document_query: e.target.value, + }) + } + required + /> + + + + ); +}; + +export default SearchQuerySection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/EncryptionSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/EncryptionSection.tsx new file mode 100644 index 0000000..9f11954 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/EncryptionSection.tsx @@ -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) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, encryption: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + return ( + + + {t("policy.fileEncryption")} + window.open("https://docs.cloudreve.org/usage/file-encryption", "_blank")}> + + + + + + + } + label={t("policy.enableFileEncryption")} + /> + + + + + + + + ); +}; + +export default EncryptionSection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts index d8ddb7d..993283e 100644 --- a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts @@ -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"; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx index d6873e6..b703b9a 100644 --- a/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx @@ -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 = () => { + ); diff --git a/src/component/Admin/StoragePolicy/TrafficDiagram.tsx b/src/component/Admin/StoragePolicy/TrafficDiagram.tsx index b205f49..56f9355 100644 --- a/src/component/Admin/StoragePolicy/TrafficDiagram.tsx +++ b/src/component/Admin/StoragePolicy/TrafficDiagram.tsx @@ -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 = ({ )} + {variant == "download" && ( + + + {t("policy.encryptedFile")} + + + )} { 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 = () => { {t("fileManager.size")} {t("fileManager.createdBy")} {t("application:fileManager.storagePolicy")} + {showEncryptionStatus && ( + {t("application:fileManager.encryption")} + )} @@ -264,6 +269,17 @@ const VersionControl = () => { /> {e.storage_policy?.name} + {showEncryptionStatus && ( + + + + )} ))} diff --git a/src/component/FileManager/Sidebar/BasicInfo.tsx b/src/component/FileManager/Sidebar/BasicInfo.tsx index c29d1fc..4737c01 100644 --- a/src/component/FileManager/Sidebar/BasicInfo.tsx +++ b/src/component/FileManager/Sidebar/BasicInfo.tsx @@ -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 ( + + + {status.status === "full" ? ( + theme.palette.success.main, + }} + /> + ) : status.status === "partial" ? ( + theme.palette.action.disabled, + }} + /> + ) : ( + theme.palette.action.disabled, + }} + /> + )} + {!simplified && ( + <> + + {title} + + + )} + + + ); +}; + 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(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 ; + } + return ; + }, [target.extended_info, t]); + const handleDeleteViewSetting = useCallback(() => { dispatch(sendPatchViewSync({ uri: target.path })) .then(() => { @@ -226,6 +341,9 @@ const BasicInfo = ({ target }: BasicInfoProps) => { ) } /> + {showEncryptionStatus && encryptionStatus && ( + + )} )} + + + ); +} diff --git a/src/component/Icons/ShieldLock.tsx b/src/component/Icons/ShieldLock.tsx new file mode 100644 index 0000000..039fbd8 --- /dev/null +++ b/src/component/Icons/ShieldLock.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShieldLock(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ShieldLockFilled.tsx b/src/component/Icons/ShieldLockFilled.tsx new file mode 100644 index 0000000..b469fa3 --- /dev/null +++ b/src/component/Icons/ShieldLockFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShieldLockFilled(props: SvgIconProps) { + return ( + + {" "} + + ); +} diff --git a/src/component/Uploader/core/uploader/base.ts b/src/component/Uploader/core/uploader/base.ts index cc290d8..61a3d5d 100644 --- a/src/component/Uploader/core/uploader/base.ts +++ b/src/component/Uploader/core/uploader/base.ts @@ -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, ); diff --git a/src/component/Uploader/core/uploader/encrypt/blob.ts b/src/component/Uploader/core/uploader/encrypt/blob.ts index f3abd94..72649e1 100644 --- a/src/component/Uploader/core/uploader/encrypt/blob.ts +++ b/src/component/Uploader/core/uploader/encrypt/blob.ts @@ -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: