mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Add PAPERLESS_TRASH_DIR
When set, original files are moved here instead of being permanently removed when a document is deleted.
This commit is contained in:
		| @@ -17,6 +17,7 @@ paperlessng_db_sslmode: prefer | |||||||
| paperlessng_directory: /opt/paperless-ng | paperlessng_directory: /opt/paperless-ng | ||||||
| paperlessng_consumption_dir: "{{ paperlessng_directory }}/consumption" | paperlessng_consumption_dir: "{{ paperlessng_directory }}/consumption" | ||||||
| paperlessng_data_dir: "{{ paperlessng_directory }}/data" | paperlessng_data_dir: "{{ paperlessng_directory }}/data" | ||||||
|  | paperlessng_trash_dir: | ||||||
| paperlessng_media_root: "{{ paperlessng_directory }}/media" | paperlessng_media_root: "{{ paperlessng_directory }}/media" | ||||||
| paperlessng_staticdir: "{{ paperlessng_directory }}/static" | paperlessng_staticdir: "{{ paperlessng_directory }}/static" | ||||||
| paperlessng_filename_format: | paperlessng_filename_format: | ||||||
|   | |||||||
| @@ -252,9 +252,11 @@ | |||||||
|     owner: "{{ paperlessng_system_user }}" |     owner: "{{ paperlessng_system_user }}" | ||||||
|     group: "{{ paperlessng_system_group }}" |     group: "{{ paperlessng_system_group }}" | ||||||
|     mode: "750" |     mode: "750" | ||||||
|  |   when: item | ||||||
|   with_items: |   with_items: | ||||||
|     - "{{ paperlessng_consumption_dir }}" |     - "{{ paperlessng_consumption_dir }}" | ||||||
|     - "{{ paperlessng_data_dir }}" |     - "{{ paperlessng_data_dir }}" | ||||||
|  |     - "{{ paperlessng_trash_dir }}" | ||||||
|     - "{{ paperlessng_media_root }}" |     - "{{ paperlessng_media_root }}" | ||||||
|     - "{{ paperlessng_staticdir }}" |     - "{{ paperlessng_staticdir }}" | ||||||
|  |  | ||||||
| @@ -277,6 +279,8 @@ | |||||||
|       line: "PAPERLESS_CONSUMPTION_DIR={{ paperlessng_consumption_dir }}" |       line: "PAPERLESS_CONSUMPTION_DIR={{ paperlessng_consumption_dir }}" | ||||||
|     - regexp: PAPERLESS_DATA_DIR |     - regexp: PAPERLESS_DATA_DIR | ||||||
|       line: "PAPERLESS_DATA_DIR={{ paperlessng_data_dir }}" |       line: "PAPERLESS_DATA_DIR={{ paperlessng_data_dir }}" | ||||||
|  |     - regexp: PAPERLESS_TRASH_DIR | ||||||
|  |       line: "PAPERLESS_TRASH_DIR={{ paperlessng_trash_dir }}" | ||||||
|     - regexp: PAPERLESS_MEDIA_ROOT |     - regexp: PAPERLESS_MEDIA_ROOT | ||||||
|       line: "PAPERLESS_MEDIA_ROOT={{ paperlessng_media_root }}" |       line: "PAPERLESS_MEDIA_ROOT={{ paperlessng_media_root }}" | ||||||
|     - regexp: PAPERLESS_STATICDIR |     - regexp: PAPERLESS_STATICDIR | ||||||
|   | |||||||
| @@ -80,6 +80,15 @@ PAPERLESS_DATA_DIR=<path> | |||||||
|  |  | ||||||
|     Defaults to "../data/", relative to the "src" directory. |     Defaults to "../data/", relative to the "src" directory. | ||||||
|  |  | ||||||
|  | PAPERLESS_TRASH_DIR=<path> | ||||||
|  |     Instead of removing deleted documents, they are moved to this directory. | ||||||
|  |  | ||||||
|  |     This must be writeable by the user running paperless. When running inside | ||||||
|  |     docker, ensure that this path is within a permanent volume (such as | ||||||
|  |     "../media/trash") so it won't get lost on upgrades. | ||||||
|  |  | ||||||
|  |     Defaults to empty (i.e. really delete documents). | ||||||
|  |  | ||||||
| PAPERLESS_MEDIA_ROOT=<path> | PAPERLESS_MEDIA_ROOT=<path> | ||||||
|     This is where your documents and thumbnails are stored. |     This is where your documents and thumbnails are stored. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ | |||||||
|  |  | ||||||
| #PAPERLESS_CONSUMPTION_DIR=../consume | #PAPERLESS_CONSUMPTION_DIR=../consume | ||||||
| #PAPERLESS_DATA_DIR=../data | #PAPERLESS_DATA_DIR=../data | ||||||
|  | #PAPERLESS_TRASH_DIR= | ||||||
| #PAPERLESS_MEDIA_ROOT=../media | #PAPERLESS_MEDIA_ROOT=../media | ||||||
| #PAPERLESS_STATICDIR=../static | #PAPERLESS_STATICDIR=../static | ||||||
| #PAPERLESS_FILENAME_FORMAT= | #PAPERLESS_FILENAME_FORMAT= | ||||||
|   | |||||||
| @@ -225,6 +225,28 @@ def set_tags(sender, | |||||||
| @receiver(models.signals.post_delete, sender=Document) | @receiver(models.signals.post_delete, sender=Document) | ||||||
| def cleanup_document_deletion(sender, instance, using, **kwargs): | def cleanup_document_deletion(sender, instance, using, **kwargs): | ||||||
|     with FileLock(settings.MEDIA_LOCK): |     with FileLock(settings.MEDIA_LOCK): | ||||||
|  |         if settings.TRASH_DIR: | ||||||
|  |             counter = 0 | ||||||
|  |             old_filename = os.path.split(instance.source_path)[1] | ||||||
|  |             (old_filebase, old_fileext) = os.path.splitext(old_filename) | ||||||
|  |  | ||||||
|  |             while True: | ||||||
|  |                 new_file_path = os.path.join( | ||||||
|  |                     settings.TRASH_DIR, | ||||||
|  |                     old_filebase + | ||||||
|  |                     (f"_{counter:02}" if counter else "") + | ||||||
|  |                     old_fileext | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 if os.path.exists(new_file_path): | ||||||
|  |                     counter += 1 | ||||||
|  |                 else: | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |             logger.debug( | ||||||
|  |                 f"Moving {instance.source_path} to trash at {new_file_path}") | ||||||
|  |             os.rename(instance.source_path, new_file_path) | ||||||
|  |  | ||||||
|         for filename in (instance.source_path, |         for filename in (instance.source_path, | ||||||
|                          instance.archive_path, |                          instance.archive_path, | ||||||
|                          instance.thumbnail_path): |                          instance.thumbnail_path): | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import datetime | |||||||
| import hashlib | import hashlib | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
|  | import tempfile | ||||||
| import uuid | import uuid | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from unittest import mock | from unittest import mock | ||||||
| @@ -154,6 +155,41 @@ class TestFileHandling(DirectoriesMixin, TestCase): | |||||||
|         self.assertEqual(os.path.isfile(settings.ORIGINALS_DIR + "/none/none.pdf"), False) |         self.assertEqual(os.path.isfile(settings.ORIGINALS_DIR + "/none/none.pdf"), False) | ||||||
|         self.assertEqual(os.path.isdir(settings.ORIGINALS_DIR + "/none"), False) |         self.assertEqual(os.path.isdir(settings.ORIGINALS_DIR + "/none"), False) | ||||||
|  |  | ||||||
|  |     @override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{correspondent}", TRASH_DIR=tempfile.mkdtemp()) | ||||||
|  |     def test_document_delete_trash(self): | ||||||
|  |         document = Document() | ||||||
|  |         document.mime_type = "application/pdf" | ||||||
|  |         document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED | ||||||
|  |         document.save() | ||||||
|  |  | ||||||
|  |         # Ensure that filename is properly generated | ||||||
|  |         document.filename = generate_filename(document) | ||||||
|  |         self.assertEqual(document.filename, | ||||||
|  |                          "none/none.pdf") | ||||||
|  |  | ||||||
|  |         create_source_path_directory(document.source_path) | ||||||
|  |         Path(document.source_path).touch() | ||||||
|  |  | ||||||
|  |         # Ensure file was moved to trash after delete | ||||||
|  |         self.assertEqual(os.path.isfile(settings.TRASH_DIR + "/none/none.pdf"), False) | ||||||
|  |         document.delete() | ||||||
|  |         self.assertEqual(os.path.isfile(settings.ORIGINALS_DIR + "/none/none.pdf"), False) | ||||||
|  |         self.assertEqual(os.path.isdir(settings.ORIGINALS_DIR + "/none"), False) | ||||||
|  |         self.assertEqual(os.path.isfile(settings.TRASH_DIR + "/none.pdf"), True) | ||||||
|  |         self.assertEqual(os.path.isfile(settings.TRASH_DIR + "/none_01.pdf"), False) | ||||||
|  |  | ||||||
|  |         # Create an identical document and ensure it is trashed under a new name | ||||||
|  |         document = Document() | ||||||
|  |         document.mime_type = "application/pdf" | ||||||
|  |         document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED | ||||||
|  |         document.save() | ||||||
|  |         document.filename = generate_filename(document) | ||||||
|  |         create_source_path_directory(document.source_path) | ||||||
|  |         Path(document.source_path).touch() | ||||||
|  |         document.delete() | ||||||
|  |         self.assertEqual(os.path.isfile(settings.TRASH_DIR + "/none_01.pdf"), True) | ||||||
|  |  | ||||||
|  |  | ||||||
|     @override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{correspondent}") |     @override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{correspondent}") | ||||||
|     def test_document_delete_nofile(self): |     def test_document_delete_nofile(self): | ||||||
|         document = Document() |         document = Document() | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ def paths_check(app_configs, **kwargs): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     return path_check("PAPERLESS_DATA_DIR", settings.DATA_DIR) + \ |     return path_check("PAPERLESS_DATA_DIR", settings.DATA_DIR) + \ | ||||||
|  |         path_check("PAPERLESS_TRASH_DIR", settings.TRASH_DIR) + \ | ||||||
|         path_check("PAPERLESS_MEDIA_ROOT", settings.MEDIA_ROOT) + \ |         path_check("PAPERLESS_MEDIA_ROOT", settings.MEDIA_ROOT) + \ | ||||||
|         path_check("PAPERLESS_CONSUMPTION_DIR", settings.CONSUMPTION_DIR) |         path_check("PAPERLESS_CONSUMPTION_DIR", settings.CONSUMPTION_DIR) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -57,6 +57,8 @@ THUMBNAIL_DIR = os.path.join(MEDIA_ROOT, "documents", "thumbnails") | |||||||
|  |  | ||||||
| DATA_DIR = os.getenv('PAPERLESS_DATA_DIR', os.path.join(BASE_DIR, "..", "data")) | DATA_DIR = os.getenv('PAPERLESS_DATA_DIR', os.path.join(BASE_DIR, "..", "data")) | ||||||
|  |  | ||||||
|  | TRASH_DIR = os.getenv('PAPERLESS_TRASH_DIR') | ||||||
|  |  | ||||||
| # Lock file for synchronizing changes to the MEDIA directory across multiple | # Lock file for synchronizing changes to the MEDIA directory across multiple | ||||||
| # threads. | # threads. | ||||||
| MEDIA_LOCK = os.path.join(MEDIA_ROOT, "media.lock") | MEDIA_LOCK = os.path.join(MEDIA_ROOT, "media.lock") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Felix Eckhofer
					Felix Eckhofer