Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 4 additions & 0 deletions ansible/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,11 @@
owner: "{{ paperlessng_system_user }}"
group: "{{ paperlessng_system_group }}"
mode: "750"
when: item
with_items:
- "{{ paperlessng_consumption_dir }}"
- "{{ paperlessng_data_dir }}"
- "{{ paperlessng_trash_dir }}"
- "{{ paperlessng_media_root }}"
- "{{ paperlessng_staticdir }}"

Expand All @@ -277,6 +279,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
31 changes: 31 additions & 0 deletions src/documents/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,37 @@ 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:
# Find a non-conflicting filename in case a document with the same
# name was moved to trash earlier
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}")
try:
os.rename(instance.source_path, new_file_path)
except OSError as e:
logger.error(
f"Failed to move {instance.source_path} to trash at "
f"{new_file_path}: {e}. Skipping cleanup!"
)
return

for filename in (instance.source_path,
instance.archive_path,
instance.thumbnail_path):
Expand Down
35 changes: 35 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,40 @@ 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