Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Commit

Permalink
Add PAPERLESS_TRASH_DIR
Browse files Browse the repository at this point in the history
When set, original files are moved here instead of being permanently
removed when a document is deleted.
  • Loading branch information
tribut committed Aug 22, 2021
1 parent cd43bc1 commit b6e8481
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 0 deletions.
1 change: 1 addition & 0 deletions ansible/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ paperlessng_db_sslmode: prefer
paperlessng_directory: /opt/paperless-ng
paperlessng_consumption_dir: "{{ paperlessng_directory }}/consumption"
paperlessng_data_dir: "{{ paperlessng_directory }}/data"
paperlessng_trash_dir:
paperlessng_media_root: "{{ paperlessng_directory }}/media"
paperlessng_staticdir: "{{ paperlessng_directory }}/static"
paperlessng_filename_format:
Expand Down
3 changes: 3 additions & 0 deletions ansible/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
with_items:
- "{{ paperlessng_consumption_dir }}"
- "{{ paperlessng_data_dir }}"
- "{{ paperlessng_trash_dir }}"
- "{{ paperlessng_media_root }}"
- "{{ paperlessng_staticdir }}"

Expand All @@ -277,6 +278,8 @@
line: "PAPERLESS_CONSUMPTION_DIR={{ paperlessng_consumption_dir }}"
- regexp: PAPERLESS_DATA_DIR
line: "PAPERLESS_DATA_DIR={{ paperlessng_data_dir }}"
- regexp: PAPERLESS_TRASH_DIR
line: "PAPERLESS_TRASH_DIR={{ paperlessng_trash_dir }}"
- regexp: PAPERLESS_MEDIA_ROOT
line: "PAPERLESS_MEDIA_ROOT={{ paperlessng_media_root }}"
- regexp: PAPERLESS_STATICDIR
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ PAPERLESS_DATA_DIR=<path>

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>
This is where your documents and thumbnails are stored.

Expand Down
1 change: 1 addition & 0 deletions paperless.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#PAPERLESS_CONSUMPTION_DIR=../consume
#PAPERLESS_DATA_DIR=../data
#PAPERLESS_TRASH_DIR=
#PAPERLESS_MEDIA_ROOT=../media
#PAPERLESS_STATICDIR=../static
#PAPERLESS_FILENAME_FORMAT=
Expand Down
22 changes: 22 additions & 0 deletions src/documents/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,28 @@ def set_tags(sender,
@receiver(models.signals.post_delete, sender=Document)
def cleanup_document_deletion(sender, instance, using, **kwargs):
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,
instance.archive_path,
instance.thumbnail_path):
Expand Down
36 changes: 36 additions & 0 deletions src/documents/tests/test_file_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import hashlib
import os
import random
import tempfile
import uuid
from pathlib import Path
from unittest import mock
Expand Down Expand Up @@ -154,6 +155,41 @@ def test_document_delete(self):
self.assertEqual(os.path.isfile(settings.ORIGINALS_DIR + "/none/none.pdf"), 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}")
def test_document_delete_nofile(self):
document = Document()
Expand Down
1 change: 1 addition & 0 deletions src/paperless/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def paths_check(app_configs, **kwargs):
"""

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_CONSUMPTION_DIR", settings.CONSUMPTION_DIR)

Expand Down
2 changes: 2 additions & 0 deletions src/paperless/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def __get_boolean(key, default="NO"):

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
# threads.
MEDIA_LOCK = os.path.join(MEDIA_ROOT, "media.lock")
Expand Down

0 comments on commit b6e8481

Please sign in to comment.