From a68b858733327313408e898c8e307df2826ebf41 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 01:15:39 +0100 Subject: [PATCH] new exporter that updates the export in place, fixes #376 #343 #166 --- .../management/commands/document_exporter.py | 156 ++++++++++++++---- .../management/commands/document_importer.py | 6 +- .../samples/documents/originals/0000002.pdf | Bin 0 -> 25743 bytes .../samples/documents/originals/0000003.pdf | Bin 0 -> 12022 bytes .../samples/documents/thumbnails/0000002.png | Bin 0 -> 7913 bytes .../samples/documents/thumbnails/0000003.png | Bin 0 -> 7913 bytes .../tests/test_management_exporter.py | 152 +++++++++++++---- 7 files changed, 242 insertions(+), 72 deletions(-) create mode 100644 src/documents/tests/samples/documents/originals/0000002.pdf create mode 100644 src/documents/tests/samples/documents/originals/0000003.pdf create mode 100644 src/documents/tests/samples/documents/thumbnails/0000002.png create mode 100644 src/documents/tests/samples/documents/thumbnails/0000003.png diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index a7a17f124..e2313e86a 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -1,15 +1,21 @@ +import hashlib import json import os import shutil import time +import tqdm +from django.conf import settings from django.core import serializers from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from filelock import FileLock from documents.models import Document, Correspondent, Tag, DocumentType from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \ EXPORTER_ARCHIVE_NAME from paperless.db import GnuPG +from ...file_handling import generate_filename, delete_empty_directories from ...mixins import Renderable @@ -24,13 +30,36 @@ class Command(Renderable, BaseCommand): def add_arguments(self, parser): parser.add_argument("target") + parser.add_argument( + "--compare-checksums", + default=False, + action="store_true", + help="Compare file checksums when determining whether to export " + "a file or not. If not specified, file size and time " + "modified is used instead." + ) + + parser.add_argument( + "--use-filename-format", + default=False, + action="store_true", + help="Use PAPERLESS_FILENAME_FORMAT for storing files in the " + "export directory, if configured." + ) + def __init__(self, *args, **kwargs): BaseCommand.__init__(self, *args, **kwargs) self.target = None + self.files_in_export_dir = [] + self.exported_files = [] + self.compare_checksums = False + self.use_filename_format = False def handle(self, *args, **options): self.target = options["target"] + self.compare_checksums = options['compare_checksums'] + self.use_filename_format = options['use_filename_format'] if not os.path.exists(self.target): raise CommandError("That path doesn't exist") @@ -38,52 +67,75 @@ class Command(Renderable, BaseCommand): if not os.access(self.target, os.W_OK): raise CommandError("That path doesn't appear to be writable") - if os.listdir(self.target): - raise CommandError("That directory is not empty.") - - self.dump() + with FileLock(settings.MEDIA_LOCK): + self.dump() def dump(self): + # 1. Take a snapshot of what files exist in the current export folder + for root, dirs, files in os.walk(self.target): + self.files_in_export_dir.extend( + map(lambda f: os.path.abspath(os.path.join(root, f)), files) + ) - documents = Document.objects.all() - document_map = {d.pk: d for d in documents} - manifest = json.loads(serializers.serialize("json", documents)) + # 2. Create manifest, containing all correspondents, types, tags and + # documents + with transaction.atomic(): + manifest = json.loads( + serializers.serialize("json", Correspondent.objects.all())) - for index, document_dict in enumerate(manifest): + manifest += json.loads(serializers.serialize( + "json", Tag.objects.all())) - # Force output to unencrypted as that will be the current state. - # The importer will make the decision to encrypt or not. - manifest[index]["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED # NOQA: E501 + manifest += json.loads(serializers.serialize( + "json", DocumentType.objects.all())) + + documents = Document.objects.order_by("id") + document_map = {d.pk: d for d in documents} + document_manifest = json.loads( + serializers.serialize("json", documents)) + manifest += document_manifest + + # 3. Export files from each document + for index, document_dict in tqdm.tqdm(enumerate(document_manifest), + total=len(document_manifest)): + # 3.1. store files unencrypted + document_dict["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED # NOQA: E501 document = document_map[document_dict["pk"]] - print(f"Exporting: {document}") - + # 3.2. generate a unique filename filename_counter = 0 while True: - original_name = document.get_public_filename( - counter=filename_counter) - original_target = os.path.join(self.target, original_name) + if self.use_filename_format: + base_name = generate_filename( + document, counter=filename_counter) + else: + base_name = document.get_public_filename( + counter=filename_counter) - if not os.path.exists(original_target): + if base_name not in self.exported_files: + self.exported_files.append(base_name) break else: filename_counter += 1 - thumbnail_name = original_name + "-thumbnail.png" - thumbnail_target = os.path.join(self.target, thumbnail_name) - + # 3.3. write filenames into manifest + original_name = base_name + original_target = os.path.join(self.target, original_name) document_dict[EXPORTER_FILE_NAME] = original_name + + thumbnail_name = base_name + "-thumbnail.png" + thumbnail_target = os.path.join(self.target, thumbnail_name) document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_name if os.path.exists(document.archive_path): - archive_name = document.get_public_filename( - archive=True, counter=filename_counter, suffix="_archive") + archive_name = base_name + "-archive.pdf" archive_target = os.path.join(self.target, archive_name) document_dict[EXPORTER_ARCHIVE_NAME] = archive_name else: archive_target = None + # 3.4. write files to target folder t = int(time.mktime(document.created.timetuple())) if document.storage_type == Document.STORAGE_TYPE_GPG: @@ -100,21 +152,57 @@ class Command(Renderable, BaseCommand): f.write(GnuPG.decrypted(document.archive_path)) os.utime(archive_target, times=(t, t)) else: + self.check_and_copy(document.source_path, + document.checksum, + original_target) - shutil.copy(document.source_path, original_target) - shutil.copy(document.thumbnail_path, thumbnail_target) + self.check_and_copy(document.thumbnail_path, + None, + thumbnail_target) if archive_target: - shutil.copy(document.archive_path, archive_target) + self.check_and_copy(document.archive_path, + document.archive_checksum, + archive_target) - manifest += json.loads( - serializers.serialize("json", Correspondent.objects.all())) + # 4. write manifest to target forlder + manifest_path = os.path.abspath( + os.path.join(self.target, "manifest.json")) - manifest += json.loads(serializers.serialize( - "json", Tag.objects.all())) - - manifest += json.loads(serializers.serialize( - "json", DocumentType.objects.all())) - - with open(os.path.join(self.target, "manifest.json"), "w") as f: + with open(manifest_path, "w") as f: json.dump(manifest, f, indent=2) + + if manifest_path in self.files_in_export_dir: + self.files_in_export_dir.remove(manifest_path) + + # 5. Remove files which we did not explicitly export in this run + for f in self.files_in_export_dir: + os.remove(f) + + delete_empty_directories(os.path.abspath(os.path.dirname(f)), + os.path.abspath(self.target)) + + def check_and_copy(self, source, source_checksum, target): + if os.path.abspath(target) in self.files_in_export_dir: + self.files_in_export_dir.remove(os.path.abspath(target)) + + perform_copy = False + + if os.path.exists(target): + source_stat = os.stat(source) + target_stat = os.stat(target) + if self.compare_checksums and source_checksum: + with open(target, "rb") as f: + target_checksum = hashlib.md5(f.read()).hexdigest() + perform_copy = target_checksum != source_checksum + elif source_stat.st_mtime != target_stat.st_mtime: + perform_copy = True + elif source_stat.st_size != target_stat.st_size: + perform_copy = True + else: + # Copy if it does not exist + perform_copy = True + + if perform_copy: + os.makedirs(os.path.dirname(target), exist_ok=True) + shutil.copy2(source, target) diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 6df14a82c..a2e19e3cc 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -148,10 +148,10 @@ class Command(Renderable, BaseCommand): create_source_path_directory(document.source_path) - shutil.copy(document_path, document.source_path) - shutil.copy(thumbnail_path, document.thumbnail_path) + shutil.copy2(document_path, document.source_path) + shutil.copy2(thumbnail_path, document.thumbnail_path) if archive_path: create_source_path_directory(document.archive_path) - shutil.copy(archive_path, document.archive_path) + shutil.copy2(archive_path, document.archive_path) document.save() diff --git a/src/documents/tests/samples/documents/originals/0000002.pdf b/src/documents/tests/samples/documents/originals/0000002.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5e75266ca7e534ae87533e612c662a32320eb0c6 GIT binary patch literal 25743 zcmdqHWpo_Dk|k`5(PCz1wV0WinW-(bpvBD0OcqNPv&CRBg9R2dGc&IIcHYd}Z)VT8 zyJvrGpX$?5nH8B4Q8%)(IxdBhm;?heBL^HsZ((nHZ)0x`91AfMv7Lz(93LM*4rpuc zWI@dO(V_y7u(WXkf&daW#!f&npsAf1P(T3A(Fp`J{s!mvT|HJFK0pXLJ)LvHXy)iZd>>4TU-Y)lD6XZx0DnO_U83;wA zoMsJ{WyiDL?+%@4(N4$1@XB^`Z6r6V{*3#3sqSrBU@w$9OUW|LKP6mn8!Ai(EMa3H zQn+q9w=a#N)_?Nomo!7IMK+M*HDb5si%0QaNwfK=}64GKzqTt$-gK(AMnV-1(6HMgZ=w<9`KaZjQgb|7hdU*Sh?IMC-g5Nwy`e+*EPK)`%{1IXLi{4iF?~3>Uf-EWh<&tJcIucnYt}~)o#9%`UA4|j- zGOnsD7(+ZeH5*fVFQk~3;R~@F#rro1OIf8H4xCsDRTPj#Ok4&ADSvj@051`~7v!J? zt0%gHpWs&Tb)XDBYlTRuiZvWuLuJDGKg+a_aj5K^> zn0Cj-9qbei5Q_=R_BzENzK=)S$-%UMje`a8FI|ycF;Rb>|fk?K2qRaLxM1f;dLOfD7`#@&o(O>XY-?D-6a z)F(!K?kC-q=r3O{6m>2(0V^6_sji=O zmi5g1)ljt9q;0YE)1mjtIof)@!d6TDxU{rNr`AimU&SYU)RK-J_oiQbsRt}tr%0^^ zA&!<2JZ}y`w;SixxHB24NmJ9IvT-Wqs7zyxXvE%_F%Zzt(%F%f|6m^i?~o3BfZNX# z#rez~3fM#^S1`^-Ls)^T6ISbXOrO_rJ3~J?Ih1^`hbP)$*{5!g?uh_8DFuP2!c&x} z=QrqC5BxHBYGn`N=!t5qNwEi$_? zge+Z1(bqb1br()U^V=}`HA9kE#$ghUx}$`A@y>wQ%<#7rxj!d2r^N_nOgnSp4^j^i zZwPo2iuI(eWPcCT8fI#?h}Fp8DDZTWRb4E`{!g&9sby^#I{8} z7^G1c=XcHI-~Pp)#vCMJtgqy<`m2LXb-KP>uvhJ&Ws1q^fqmY1lSLMv>?*J+?&82~1YGD%S<1N@xHBINK6 zavzYUkG~UO(6eSVOAr>L>|5^YieCNVFW~s@*DKW2RwS}8m4l*#jNAhn&@s2-r$+f= z+%uqK!NN)?kWI7eeZ7b=Zbfi>T1;A}_2VbEsEBBt+Q5v=JjOf^fmOU5^8S|RQx3&i zOFirD_t&7**+iqrAsfp9THQIGRU(`TF3~%fsy5@Q^|KKe;W7KT^H`|Jhi zNZ{6Hma*u|ZQPey_z{=e{bXZXOwF9aQG3whBQOH}sLV(P@g%<9ck!Egv^$qM)v>{} zn;M{nO}QlkvL%LM-F98R4$QPH*nTvArr2TNX5p;dlFoS9y+e&ezwSspJLfZ^9s0!(bRN;#eCxy^ z{Bk=6V_)KG{$3BohefzF_09$^=QxD*T|46En>qIi8+8gwEvX8={g%*oOWrSRN$YPF zLQ)wj80f--qQ!nn-zh`ES)3x`?V+TnSjwb`5meAo#fKc|kc;E(Eh7r%5l}=a(P<&0 zm52}0NCyPKNslc1_(Jy=z=)}wlpyYKl9m*vii!DEi5Dnh*g!A|!J$u#WoOHaz9OM* z^;rVS@rvy=dNu`VHY*xBEn!;JoDh=5qQVZfDDXd^-i4tal9{~$$KM;Ra4TPz0xb{Z6 zK`{g)_TZou0NE-Q^T}|jjo355$aK3x0w62GNydcY(lY3Co4T~&zc+TFz@IdA`9UHN zE5H2NXSfJ~j8WEE0b9kiR|yE3)Y?#%iBM+l25U(2*V+J=kyrI%=r@F1@xOZqKu zxlbdII70$!ueQgRWsy=QtxB7*H+I2bX&LpWm!Qd@xv8SHHEPk@>oOP>MTMo&N{`EyFDD}@ zODxh*b{!=oT%ni+w-U24ms3iMsis1kDHBY%-oSho4_vsTWfl&Oj?Kq5xIn{bO9Mum z#0at*u3lqEYZIFQq`{=+ueHh7Pi!r)--C-H$|O|4l;fjf%i10@T>P2I1GumLk^n@! zEoc68B3@jLRaOp>SEfV#V%vDKAZ$-35I4}1Gav3#r5OUmQonD3>awr)q+_EIV#j6`*b z=heZGXm&DUcy{MEeb;lFEn&y!ss119D=SwI&bN{J45Dv%F`MmE^HzN2=(73riX^8>LWM02#;PfP%V&z6R<2~r>PVwI4RQe9i7rPeWkPr2zdfJ z>2p*-IWwopb~u;Mcj+RnaVL+bkbf0e1)aS0e$yrEke%~E8X9jxz_-LHGc|n4%w)u; zFJH_0w0w5VSs&5XtLgT`V7vZHFzDWFz!Ghqe(pyZf@33GHs)Rw&!2p8#yS&m=LXq9 zk7JEL5CP>kbb66dtyZqi#0Rr&<3L(wo~rLWXuw(LGY;;aOGeZ>~bWxGAZNtrM9(8M#tP8F0h;!Jz& zRh&PX>_S`qKp*(%qCQpvaH$5S{;ZCn3YshJUtY7|{smZeeXhNlo1?71w4Za;H8gx5 z53NrBsB$n1(p9WHbi_g@=sS=#2-%QPS{Pau5fv z?;CQyR8Tx`RxfdHe;k9&shI8Y6h!BF*b}in(`KR->lV8?L z+15P13|vC4E5k28DxOB6N6UCqIYnq#<+31D!sWaDo^?MzW+Q8EHFeS=(-Vsg(}D}n zy)2`uVp0o!MJj*H*TR3M%9YZ4Cjxj*438e0R#J9rsg)^{21 zrZji^?0Y7DRlmD#IVCcEE+cOnjy)d1i^5U%&W?7^+v~S`-o^!bzh)-h>1Z}4jvEox zp<;1U-*DZ)B@SEB{lsM4to)&}LLhr8u1Bn8rfs9S>=^*p;7@_=+|ebP0o@~N%7A3< zo>sJtj3h!B_4>9IZ`8EY-*1w~;BMRz#@`GfYMvYeR)p}&tOZ#6vrkAz^{`3cgtu zIpxOu^`s4sS38EN2#k+(Ox>MtPDTCkN5A!OL_TR`aWr|ixwn-{4oH)EXE5sG2k^f2$Fkj#V|BZ)1*-%oPdt>dw7KW3JuUABy|1di zZ@1M8enVn+;lCPE%r2MWp1f#c*L+&8yRWcmCV34c0xHU#Eb;SjV4K*!t!+PLM#q4c z)`mXc$+1QKOwpUVxS4Y$@)9w`b|gA|0JSS?;g)w)Y(srL8hC}Smd59@uWU{MTZnvZ zP8l;p;if;K2mhUFJ?3$3B8gLl`m} z+X)DLwrbIk!a8lm4SfUoTOfY%OpCadipY+PAixFoYMa17$X>`r6QZ#bPS1w0pc_^2 z3l9Y3pniKa#0v6}Dx{GB6F~=H#hsX~-N}KzkKS)(moeguzGJ~WOa+($-XN*%nO2P=|c-Psrz2qXtTH9^{fNT zeM}2GYT>=TaIdw)R-d>xbtQ^YsKfvKp!ov94)uf0?~C*@jYN#<>Vy249-%bPmPN93v9;h& zW)|<7d>=>eec#FxeH}-~RXu+>6C<7jqZbw5Uq=Ps&QbyVu6Mn@ZrVJiVGaj9Tv}36 z3x3Kq)C|~={Ml4&P_i_-OK4^c7LPx?6em=P$f1$nlMtc$qHWe*RseP|_dZ0i zbkt4xUTtwq%0jmyB_m^_lcn%Y72rbO1K1FMLdDmz@i)rrw?U1ag1W?hO#>GIb&Y=M z63@36BKvaYAT;tB1^UEZC=&dLpl0^7i_E~Ek)3LFy*xV}BOjc}?d#`_F5iPlx6S(t z3aXKmZ@x_mPlRWO%i%0U!UQqWtrHZ2U zPb$CQp>qo@&dGQcm)4L!qGK#80>7gdEM^%#!TTa6E?XDYhpgbP!Xp*mj;HRQHq zYgKSzOrCjNq<2qI{mMM`*zYNiB@6{@Y`?$S+3Y>L>Tx}6>}%(A(Gn)JO;%AQ55)5-G_{Fj)5(T&+!T}fZ?YB#$t@oP|xqZeZO{m&xv)^~gJ8{43d(@PT8#2CElr^W7gtb|ThnMyxA$Z8- z76zGz_ihN;;eW{nC_=fjm&-$KAN|ajDi^N&IvPYvtZv^37o#p*0%xCgJ#yS~HL{}2 zx}Pff1SwR7EqAkHY&Dj}rB$75n|e?#>4eM36K$7}(1X;_d9*9Z;l00h z$Qz5_^3{uWHLh&STU2s+;Mh=Em5bwLnyiPaPHWWek6L) z-1gb0^bwj3vw1ynbvA9S z2HRtG@>4sOlS+LqQO^#(m%dJY!C-3-%kH+mF>=zdCT zxJ&@Be7kWQ{d7|eqX`Hq`Qd28M0*M9nCe@&xH}k{b>ph#=0?wo5<}BtccmFD+8)gh zFXSpI*H*7TBE3{ut0l+UQwg^x=Rr}>8}ARnTI#0ACo>&ZWAh)Xm9DD((G&t7q&hW> zj+Gr{&$|cN78*Jjl8?@fE??L)gfAPNU!*BdF^~{nOQNh>_jS#`-Qwm)zGwbQqdhH# z`~ao*Ci#)bs7sXmK!WFo}?g?Nh?d#w}5IR@u&M^KdUl4VEMditJW7St%SE}GJW=(}%1^ySDBRc{?zC=W6A9uD4kM`R zRk74E0x_@$YTanH6Y=q?!zm>AR^Jq+MFxgo)@a(R$&^t^@Yjs-Ls4)^nNg{pr2;_F zq$y-{$hzU@JSwspo8$2uI}s$__`BTGQTAX(edcKscw~u%P)K;=zRrz2L5-stKUEKD zgBo*RkSBhJg94?-b5hAV&{;#iX(Zv3s9H#3GiOSp7Ud72<>BL(4m?+nU(O$-DpPYS zmWsA%Og$fyzc00$a-m6*W#yw!O~YEf{t1iBTjA^v6z!xQH$bIP=85>N-QV&YSC9&V zEGfheNB_ei-QwaEJzzV_6?G-bmdHIp!qcRMGCjr|B)OYr5PYei1W&5QxUY-K?#m8t zXo%M_+sd!D&k=z&C@pin$wl)_(QgCYKpLXOjOjUrs(elz+{>1YLd!6>P)N@mratIh zK&X&{IO?8W$a6QO5y7GkY7aeRmA+NT{}QuA1$57|jTxYkD_{hAfR)mW@CH3Nyev{x z*TF_b6k018NO~TY;V1KF=U#&f;nzJa)%z^gDD`D6VgM52Wtqk;3f#1Kl3WrUL;bZG zdlEO~G7K1bO!|+3)XWObvW?NxM=&!h61#fECD1Ky;%18C<=g10@;a9KVaNQmgEycP zv@Ap30`%^eC3Hs?g;V*xaenE_=&@&NaoruNN zmNqJNbJz{k0fH(6MN1KRC+Op$KeG$16uc=w#<=Tg^jO9EG!)A$!t;aFm0eugq0q8O zI#h47zj?+C*+s81O5?xXy@c+HwQaElJRXxV+l@jjEznfI4L_=LCO7#wrXTbZ&PqRv z>s1l`!sk9q!0A(Ww46+O+7$s5TH*IPL@Xs1)>h;RL=ku3u{)tY(ktwJNq~9Gu>L_@ zqT;dX&H>%pd{ z4ey-HjqW0a%PPJ%&@Ps*YQO4AsU7q}w)_J(kgX4#|bNl&{MT>)yNU0M}NmW$4ke-iBh;*qQICJ%qXI4{o@y-?etFH#RnfE?!fGBY6ixl%jFM)##Pi|5?aM2FN%+f&)VIFhnMg^p`*V_ zR@clgGoGVz&sp7Wy%CJ^I|?TYa%NaPUj2N2W&cT}>?vf(4N|pt`YKtyyy_ss-_$`? z0DCghl_w`&{ebS_@>MOArOJ7(_Hvk%mh_bVvAiZ_%P0qrhh73(u2-%+vETo?m za|@m_k%qIQ>r}!GE~k%wS7PU^Fo@#XX-h!_RaY$m&R^FoZop5mNlG+`8k)L;SGq4$ z%1Krv-$^Aq22K_UZo%c9h=PJ=@xQ7B|E=);tpszjbFuvG{|A?V`CrHcYVP(xfP|f` zlNiv^6l7`dWcR_f_&8NC{sshy2><=i$yu5JLB>v&cDAZOkmXmnzhn$^M`E@Q;p0jX z5j!_s26i@9Vg^*`g_!wo^;!k)qZIw`PW+Pw z@GpcB7S{hCx`=*)yj>s@Ldf|`1b&++By158s>qQAox?)tz#TX-Gr=^BaxV1St4Z5( zA%Rfr_WVSoyq-q!$erWMk)(bved`)7(u~_Rt{D9MnuH4M%<&`PlrB^3-@rZ9mm$=`1Gfwibax~w9>mV zxhHtXhY~%?u>;Av_fwqoS2~ls9An?%H`N=OMc=ZxMc<6++%vT9_)P_04U(Jj=Nc#- zzHuU|b=`+o&Xt3jh-^Fi&Fr}U3S->=K`i)pGyzndO`QHhOAX`<{70*Zu_N&B{rsPU z$`a)0Bx+#{`U@60Cj=G#5z~W$LB4%e|BK`|9RxWO07Ir3Lwh!q2 z?fYl@-@d={-|yePf0N<P|%Lj@oYG_y4Rn~HyJ;`pF|aB{Qg zf8-L#+0^O3t2+J-tR%?J+5RJCR6p*Fv8|*1-$6~?0ivoOIq70)3RIC40Z0>nXnoME z0HPlzf6VzY$KN^sCHeljtb7dgf7QT`?EgPA@b8NDuLgd=y71qh4xmNp{}O!ZT`Hv;mo%A@a*F_vptMEv;AxdDXhjLo`fp= z`Li-QG9(5hiki~L(Itc+q-=EEKDn^{`sL2Galbp`7jw0HCHvsJAQIN6mowwe%~~dc zPn#$X%q{~S?Gubu?P)-vPd0ui)L;sB>kHKSczEy^zXs+UKfNZJlfS#qzpvfDBLw95 zRscRB-}yOu6_{*&X}<7#MBUDR+nfOZdV_d%$Gg!6O^@QpwDH;QMfLs>sdvbi>%#Es zYp5yy$qkYg*e3&%jd!D*_wG(%=FLwR<@h)7Z@j&}0r=0cFanp|#tsSHDDSQ^EMbr? zUDyYo4K~2HgqdK#9fYokCH>wNQTB7t?-d&(c5b7iWMW{yAVef?M91FlBZghxBgww) zpAgx-oxWA?)qcJLz0(Mb@qbc+e|{DW`qY*5sm9a8_g%5~?d{C{6Y}*Z1j(0spB?T` zY8*&M#qYXk70`YO;6m$J?{i+XpPF6pKh0rvUNXbJ&mFv7vws377y8WB^$EX==mh>! zh+?Fhgx`p30iUu9K?}(xjKKbrLKmXjm!xfQFd<}^&&I@X7D5PNMAX!fh5iU};8#MM zM$1Y5a7vI?LIf6I47rF-U(n~Fp}H=FJkgBc#{3{_AT+vg7YHAWk?^}d%A()>;q4*M zy1?DQ02|&Laexc(&&0?O1YGDI8|n}Kit)s_efSU9(5OPBQeeZxkc9zk_ULAQa&eG# z!i-!{F@bz>NN_?VT(A*cmiAESfv8GQBf^j+5Px!^W&?PXkS6@0>)9 zrwZ{Ne9{!6IsjYEg}w<8ut(H`=Ir{^3=7qbj1AS)4bu!S(T&{-@Wmuf`V4h6q~=mx*$54<4s`c!U&=mn9}jdg)x<7c-4{Q%q8#qscQ zM~Q0S@45sagqebZl#ro%MBKtY#S^oL`5%zb_Q5a<(<9UMeQ^-dLq_U@zZ33#uT&8~OtB`y#9m z2fqW?BAWf%cnI$*_$5z>8V1h4Cl`hqH6p|$&yD)COhAlK>2GCff{~!QJiP<1B%~}q zxIEGWHZ_D~kzFN7HON0g&Px2VpY;7Qaxo7`onWm(8AY>{IA_6cg8~^$?*1!HA0y z=c71;iS?m~;gW*K2M>wwkia2}hp~pKh*9K2IbcrtS&024UP2}aQ|MC{BiiEGqBMbF z4!{=Y6XFx$laM3JCfP%-Mve?~4Z9Ch@1yGD+2S=pyM(`lt_=-}rzXdg7E*~*BS%ZX z7!KbNz9I{bV=IJE!KH~+6DcEQkLQu{E@YlYunhkx&O@9PXDg*xh%`-v6NDftL24Qg zJ7i{t%L1Mr$QMH|gh>(^4;-R3W4OY<0;_{u3~7%#5xFCMj^~UQ9pW8g-r)kFK0-Vq zaEE@E>=b4s(TpRCKOQk4o}=bXA{)Wj4ZjkG%neZ9EU{4Iqs2)M84=sfy;2G;nJH0GqbS34{_-cx zO1z<otBxloW?xHwa2_ivj@4yzDMvC@*3wFu|9mU z`@}R}3qK{O$V9v5yOux&lLkIrw0c*WNkh$(7Fq?x+?V4Z=Rprs@S3~@Y8$>g7!HEW z!0|y$W7(SI1)dXk8~8?o<>1raFGiuY1q%)*rYEW=@F%1$IBg;NgKu4LJ!f0UT=X$P z1zTG7dR#u=VHCiqB2@=0H=Gx?D05MpA9!T-dnTwi9QEqz=$+ z{JB_i6K%&?gE|j$?e*LSzc7Bl_97KP%!!`t#oCm;RDbaBLhD3a4?pi+?^_>O?^WC| zzi@njd|`Pbd!zF~dk_7D!URF;FJnYuj|Ks20$~|IDx8%o%S5S#dIHM`0SF)>2|^Z^ zA}&O&gK-Zc6`{$?u#fpIXF=tJ_5>Rk)E4B>gTBdaoP7~fBk>@!K>k7^B8CtzW=33x z;vU{32Ahws99$yKMxKH8EnGniDW8Zr2wPl^_y;fe$@kyIMC zP)&t{CdVm5O`(j8Jz+^oztFl+c$)f<#4_V1;HT7+(39Mg@Dhor8A7_;S5n8g$6>ZQ zHtp~VX%6Dd_;14#W&qG9?kId&0g(yv{rJcs;Ev7}BnYo1az&AlJT`$S-h2o!Y&TrK zQ@z7*#Rw@)qEv$Z@%||uL@KUaXrXjN^A%k;g0(*qRS34YPg#XpJIQOrQih@^ds^-= z_eKednk>m-1Z@N#7=Pt|6xsFJEMHKaNu?*c2Ibjbij6qa6o@>a;SIcbr5iv ze~@r6eHb%^F_k>^XNrAVKfg`JHPANb?DH!6D*h_zD%>j1D&{I9e?+#_q|~G^KnNfK zkgS%embDYH6FnC@7blV;63!9Ik;swtP3TPOOzDh&Lw!|$`uv3Q=aNMR2otmUz$~#Y8hu4b;vw}KZEy!{l~c+?-iI4^R&NnD{#MwSRAHZya+3bI>K}lxrEM(W+6R@k^t%O|Q+i4XaJ{ zqCm~Jn$sGx1?iKqllBvnldF@A6NwX!ld6->lg*Rxlk^j+1=+>Px~3Y}TBMrTI>oxl z+Fz#cW1YjDlm6RA8yj25n{k^;+e{lZTlRYwn=sp9`%?Qu`_!8y+ZLM#Ti6@eyKZBb z0jA;G@PiywebO6dTRWSD4kM0MAY8{KP^-g}ql5$D=GZ3vR>cnAO=fa!nRZ4owtIih1{{FICohO)Z2D{QDlG zx8c@_c=p>Z>CNuV{mmH74bAc`r!D*~fz75ZGtJi@>*aB?ZcAH>ox}Qe_FneB!RFh< zYbtcF=`wTm^e*>=>X_=_OwY`?W8}WTB-^BQwt6-ZpJ%(4 z0hEEQ0d6%|HAgj0HF!0a9gH2H9hBW?J8nC0JKl5lHIy~jHSRTvHRrW;FDU^yBHkS2 z9E2R69F!b}99~0WLxgqZPOnatPLEEGPJ>S4PD!tNuk(k_i`Om5E7ga^$KVIx1N%et ziw#P z3cFDJO~K3|jv#a(Js?6L2;um!+^A|8FLaS9q^O2ysA;JIB@7mf2h;~-*d%H6X>@K9 zH5xVQHInvH7kL{M8wne&8!^Z_VYGb=VrVMp(-?K=Ae1Yt2vix25j0v19;#W!S%8zK zg^Y!Ph4?|}?@lJ+G;u8mWjd zN`mr(vV$6e+Qe$a0>m2oEW_(jQ^}twBq)ARU{Z8Sok*FG<4`=37g4a2%To|gypk7C z#L8ewX-IR(8YH(faML`>faF24a5Un8eg+DXB3dB*F7Yk{22C=pvq+gpnRJ=1m6nyr zQSxlrYzmu5hCzm|j);!@lGan)&se9V8|6c=L)t^kL&8IpLz=_+>6}8}(Oe)l@Bvr| zWCM-?^~}J4TV{1;3ZvhXEmK<<9ToS|#}dXk)4jD5=-VnvDiSLEE3!_E7hx9F7SZaG z>&EN&>&WU<>)`8hfUY|=pfOMY2n6B*X@Jf^B%oo?;O@u{;GOJ(qs$rS38${foaXc}%-W|pT)J?~Gxv;1-PT*X|(TqB1>rns({uJ*Fl zvNW#}uQab>n@XE{o8fck9sDW#={gS=0S5sy0XqRJfo7Ic7J3#~mQI#SRwfS`PqlOB z$Mzz@xy8B2xxu;88R=N!@as|b6o2({rKhB;gsb*_F`-PXw4#=x^rY6LG(ZX<4KS?M zsg}1BI?r2cU&HFq_pTF&?flsp(^=WsTpI!#Li!!?<|!Td!p=MQ@G- zH5n$EGMPFVD;Z}2Qvq9ns>FU&Ym`LP*Qk`}#wg~2iUBrLXH$4nq-~gOgl*P={b70)yb#^l5BX+cQRu)GVxMm)G zKf8WT+|(V`9R55^IQ)UL6!k}LOm1~-dTf+_iGH+Vwqo%lV-dVw^Q+<5-T2|y<2c4x z^jOw-;JE6TV#a)?Yx+8~@7GR~w`DN9Zc7qio~g0X5i(-Rx!m= z!nn$c%FOiA^fJpF%Y0qWRsOF0sPrgj%6es-2E7Iwoim**9iSY$9KRe?9;pGYfu<3u zv0fQJH#Wy+?QBhV+&jlS*YdFe_%_E`NmaS7^IjvoT(cCmY_YVk@UYOgjNeq+cvyo| zm)@w|xY*dg2wIX^K3d{gfLfwl#$1|Pc3E6&$ZF89f3=pZ-ELB?VQqA5fNvVDp=-3R zJ8tr{XfnDVC$yZjQLLY@X==Q$jcuB&ude-NX13v6OFTB=0)ZI@qMzWf> zkT;ySzM0n>(0eahB3cpJhh#}m$G_uCeW=u(S0CE?C7LjU_vY|-HrXHLA?5z5m8ntY z0p|JIk=j*!0i~c;qZCs?%{>uahe{Q(bBQQ{2BRTnqrFLZ|3c+P4yM^dG+;< zX&+U}aL34N#C_>~*NIFiry{fvZkSfM1 z<0_o0qN>U&PW@uNRDItTlohv?ozed0#umhu@#f0rSBK(e_-4Ac%r+vI zSjS1nYUgvOU&jLbZ;RkpMpwD+79I;851#m!)K^^RQ~Q^{O{ca)spb-AR0n3dX8KuI zSV!wu>c<_290wfxudA`LQsYv~Qj=3-HS;z7=ees1tDz05J35IZi1xVXy%#oWt~W1w zwL?dd40-sR9u7AfdfV32>`?5S?eOe;&&k%r*Ra~3JE+>%J?tKlz1+Ofz2&_5yiL5X zypFtDz0V%ouNE)FAM2iFo^c;qA0!^WK5IO=Jj6UqJo(%OzNEeOy*j;o`PftxJtsdl zJR&~w-|Aj3Uz=YPKUzO7KSMtjKc+r3J*{8m+!@}kBj!SdL0CX7KtezjLZU0c8cX1PujAh)56Hj?IPrz-G^2FBrxwMK8sBprjAcrs7` zy0@FYJE8ljd!ieqo4Z@RTQfj1&=ArtC|8tAbVo!^gipjo^h)$d)I)SYbS=mx7*+I9 zR9&P}_mnKo1HK-a%49UxU2Gq{Jk|+{GmOvcj-X@Zt}oCZryvW`=s>VdI12 zu}G^W6~ukQR-#S9P$Q?qK@o7VBjIH+M-i=94am2w8OY;Am8jnc2#~XI^-)(b`SJPj zItT-!HpAj0nZqw5q{FyzX0RQ}_N2$G#(d}QEG z6_^z+Wu&EOWGoeMX6PG?TLPRBQ0H#avCHxoC{hj!Dx z=D*E&+7m?IiE&Pqd}pQ`%AGkQkD#Q?Jtr(!9&P6}_}Q z!apuk9Vmol{TAGGAl>XMMZ;2z`kp3Ot1U!>%4n`P2;9sqbC3%Q(w^GQsFXec258$xTu^LJ`V>a7AqR?x$o~BCoYTMh;NBQiD!#Ti}#D4ikpht zMYt$1e^1~8dW}4*jg;1@YAWz^H8@$_vyECV&g>PxDi-Hd39LGLT-_YuJRt1j6tQxc zi%jlhmkK(*j*X??n%FS=)=Jc>IygB9J2+n2Uyg3&P$Z%1gbRn8ORGprOG^&FCb&}g z%4*91Wf^3^WmsjcWu9et&|uMV(%;hxsu*aj zX$u&n$p~{{oeQsv|08@fB407yJYLOEP+4^4oKIBYfth%Tq)8`H85EE;6J2VHrXIq3a+g! z__TTFy(&Umhnj@yOPWa<5BA5@#uSZiu*?G+Mio-G8LJso@<86CDA z!yUUF{2enLc-}h#;Q}+AU7ahP@ZL?&n9nZN4tlB`drk~*I>QZB*0I&zhg3H+Ptnh> z&-KrEPoYnS_wOweF8aHz+qccf{qwxliU#kXK>I+K-Uo@U=&@WTbXZ^5qtuYpo7C}8M?8|Nk}Jgd!TM!OqWk_z)+TSH>)jb>^-uG$ z3+Jxw?dq}X{+VrTT@-CJA#OAg13?E55C4;Ql#krkv9I)?li7vc+VEPyeb_zuS^7GB zhwGNXatHMN+?nZl)vz0{kotKKeCt6Mc29TT_8Nt8J`>Hm)Psr;~39a z^BB%3q}X5XFoahEv+f7COs~eTy?LRLNcTjSeA7NVj|T?@^|4Sy417zO8`-}zF1~Z| zIJlVDn;4teC77#tPFu5h*ZF`fKn|dH=5y1m!B8S|K7sp(yN^BMZ}j#5*50)TMRi{B zSnD`tnvynC$Ye704tXFG+56nxdskpb_F*g{Ezn8?mf7syT~~JZ;y&adkF;shB&~5u zOs&C&v=h^+O-vw0Q-STIJ|>KU6%%=i##jT65acZ^^n7<0*uB`PrNc}o!<|{~z2~0q ze82CU?|y%rdww5#F7Ij1{OscR!L=DFX%D1*v-+Wgfr`qBw|YKleKoS>cedZyuJydx zQ`hsWmdLl&O;@ZZt!peFTQ)U$&Tee_i~2yqYsK3JnLDuI8NS z{G#(xvSKieE> zDBB-1-1?^Kef@7#sC?tEj~{K!-+L+UNPb>qgCXbHsIlm8M@>e*f`1_UHC}3X zEdGah`AUs#|;e-I1;;@X3K)d~eOiHP>7hT*J4|U4MS`{LY_^ zzRop}iyhUEy{YRhptL4L}h8^49*?Fs{ePsQIyB}VasJ4{bC)tthw;sIu>c!ZOwD+qU z)w$Emw5^}+wm#Rqta?y=tfG8!d?NZZ-k6u;d#j8az23BU^k%DfzubzBPL&adFs@ik*c0! zy~6J9eZH!4^_ai-mrhvcietSuI-9zF|Ec<M{R8KRsz3$48Ew%&YPZR2-hV zd}m@u^wE@#qn*3j;=AkLKk?qmen@ln>-t{n8}2{X|6JK0%3A!8hx0f1H&0AVPp#el zz>5_>h_);%cxu_-p83YJhqm4Ki{q0IWsj65jQSx4f52T&DbwxMdc4Q42gu~}@qYqcBdX0Q!9jRN-U zQN&X+G7i)cf*vA4@Y?xT_5mo{cvL}mJrIN~fjAo<9I9ooImAY}JFofY4c5W+{ z1a(n?(Ui}7uxEIy&6ya}f5I1o+02PCo8_rQYJrB|YI~~0#b=ak$~2Yan>cffIw>Nd zNLA!0a6m6uk;CqEtBMk1j8GKFtKb@m@tC9tEWu^AsM59ipbp$6#%%R?3RE~=SXd}4 zq-BE3ijy42;RJds6-uqSPH*&ZH{5K;BI2+~@%s+f~0wRxpJ&vV;{J+IBD2MQXSdkwtRsg3T^PWWlamq=rioStQ3U*zDIQB6ImO zAe1J?6r$|d-7Yg;C7a-v2?7Rbrtw6^! zI?YCx8BAazj}I=a7^3?DCvY9G8Ut4;K;jD{1X3Yn*L#8?@%b=9 z%-$0Wa%L;@nbhjN;1Icc0U!ZF<$NI0jBU7Ctzhz^=pZjs!BNe*IVD@<5@VumOhlM2 z@iuA`nh*|{&vfy8x*#C4A{7(eV>BWf0@!rH&ZinhW@RwO_>X|+97HmEfu;%!M~r=V zr=p=?r<=SEcXkAUWn%%Txe+8>2LR{7PWjn$uyaFLnIbhb6gT3Hx&fPnD;hCJ76tIL zY*E02r`XJHu#U!riJrp((Lm2Mf*1IwbV!G^l&GqK+_hb>f`g#rAXPO2Pb=V^>r5zP zo*l4^XFa_Ib9(Lez#jsPhr4(-Y#WVE>uTN^|C5Y3Ky7s8Z-DGBOeS>6n}a-6=piIy z@6Hwz`suka%0M<9Si)#Pp}<49m%+Sff?lWB6NFqz5K4+56darxI0JiFNkPzNvbjM$ zMibCcfUc+ydJMEzD1tv|ub|-va!?^?uuJLSOn@!1OK=CuudddrP==KxDLF;TX)(DP zOAxVfA#jUayalEMO-F#G;DIURa$1gAu%L{S1NmNjhB;?s1c4~Y=m?QfECe=8MlmE> zV8UgzQVDY-Oh&-B4Vby%G8mE69`d3T3q^ATL?OH_$D+uD%Lq9qZA%cW0sQjQ_SgDKzEYdze zHqU<~tKg)4Pz;4WDTVb*(Mm)Jm&jP4yGvypH&2hDY3Y2S82P+8L@`Pt#8)1d5x#D@ z(C4!ZTL~-NCsBk^^j7*@$eJpQ;YO&{?uxqV8^ literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/originals/0000003.pdf b/src/documents/tests/samples/documents/originals/0000003.pdf new file mode 100644 index 0000000000000000000000000000000000000000..afbeef5c8fc783d55b40d5a642a6d523413472d5 GIT binary patch literal 12022 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 Iq^$({f5Aoqy#N3J literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/thumbnails/0000002.png b/src/documents/tests/samples/documents/thumbnails/0000002.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a76840103812c371dd78f2e8bc704ea3eec1ba GIT binary patch literal 7913 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 Ruvbl>s;I6|ENAlKe*j2SP+b53 literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/thumbnails/0000003.png b/src/documents/tests/samples/documents/thumbnails/0000003.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a76840103812c371dd78f2e8bc704ea3eec1ba GIT binary patch literal 7913 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 Ruvbl>s;I6|ENAlKe*j2SP+b53 literal 0 HcmV?d00001 diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index d6ab7eadd..d6e7ad6e0 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -3,6 +3,8 @@ import json import os import shutil import tempfile +from pathlib import Path +from unittest import mock from django.core.management import call_command from django.test import TestCase, override_settings @@ -15,49 +17,60 @@ from documents.tests.utils import DirectoriesMixin, paperless_environment class TestExportImport(DirectoriesMixin, TestCase): - @override_settings( - PASSPHRASE="test" - ) - def test_exporter(self): + def setUp(self) -> None: + self.target = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.target) + + self.d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow1", filename="0000001.pdf", mime_type="application/pdf") + self.d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow2", filename="0000002.pdf", mime_type="application/pdf") + self.d3 = Document.objects.create(content="Content", checksum="d38d7ed02e988e072caf924e0f3fcb76", title="wow2", filename="0000003.pdf", mime_type="application/pdf") + self.t1 = Tag.objects.create(name="t") + self.dt1 = DocumentType.objects.create(name="dt") + self.c1 = Correspondent.objects.create(name="c") + + self.d1.tags.add(self.t1) + self.d1.correspondent = self.c1 + self.d1.document_type = self.dt1 + self.d1.save() + super(TestExportImport, self).setUp() + + def _do_export(self, use_filename_format=False, compare_checksums=False): + args = ['document_exporter', self.target] + if use_filename_format: + args += ["--use-filename-format"] + if compare_checksums: + args += ["--compare-checksums"] + + call_command(*args) + + with open(os.path.join(self.target, "manifest.json")) as f: + manifest = json.load(f) + + return manifest + + def test_exporter(self, use_filename_format=False): shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) - file = os.path.join(self.dirs.originals_dir, "0000001.pdf") + manifest = self._do_export(use_filename_format=use_filename_format) - d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow", filename="0000001.pdf", mime_type="application/pdf") - d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) - t1 = Tag.objects.create(name="t") - dt1 = DocumentType.objects.create(name="dt") - c1 = Correspondent.objects.create(name="c") + self.assertEqual(len(manifest), 6) + self.assertEqual(len(list(filter(lambda e: e['model'] == 'documents.document', manifest))), 3) - d1.tags.add(t1) - d1.correspondents = c1 - d1.document_type = dt1 - d1.save() - d2.save() - - target = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, target) - - call_command('document_exporter', target) - - with open(os.path.join(target, "manifest.json")) as f: - manifest = json.load(f) - - self.assertEqual(len(manifest), 5) + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) for element in manifest: if element['model'] == 'documents.document': - fname = os.path.join(target, element[document_exporter.EXPORTER_FILE_NAME]) + fname = os.path.join(self.target, element[document_exporter.EXPORTER_FILE_NAME]) self.assertTrue(os.path.exists(fname)) - self.assertTrue(os.path.exists(os.path.join(target, element[document_exporter.EXPORTER_THUMBNAIL_NAME]))) + self.assertTrue(os.path.exists(os.path.join(self.target, element[document_exporter.EXPORTER_THUMBNAIL_NAME]))) with open(fname, "rb") as f: checksum = hashlib.md5(f.read()).hexdigest() self.assertEqual(checksum, element['fields']['checksum']) if document_exporter.EXPORTER_ARCHIVE_NAME in element: - fname = os.path.join(target, element[document_exporter.EXPORTER_ARCHIVE_NAME]) + fname = os.path.join(self.target, element[document_exporter.EXPORTER_ARCHIVE_NAME]) self.assertTrue(os.path.exists(fname)) with open(fname, "rb") as f: @@ -65,24 +78,93 @@ class TestExportImport(DirectoriesMixin, TestCase): self.assertEqual(checksum, element['fields']['archive_checksum']) with paperless_environment() as dirs: - self.assertEqual(Document.objects.count(), 2) + self.assertEqual(Document.objects.count(), 3) Document.objects.all().delete() Correspondent.objects.all().delete() DocumentType.objects.all().delete() Tag.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command('document_importer', target) - self.assertEqual(Document.objects.count(), 2) + call_command('document_importer', self.target) + self.assertEqual(Document.objects.count(), 3) + self.assertEqual(Tag.objects.count(), 1) + self.assertEqual(Correspondent.objects.count(), 1) + self.assertEqual(DocumentType.objects.count(), 1) + self.assertEqual(Document.objects.get(id=self.d1.id).title, "wow1") + self.assertEqual(Document.objects.get(id=self.d2.id).title, "wow2") + self.assertEqual(Document.objects.get(id=self.d3.id).title, "wow2") messages = check_sanity() # everything is alright after the test self.assertEqual(len(messages), 0, str([str(m) for m in messages])) - @override_settings( - PAPERLESS_FILENAME_FORMAT="{title}" - ) def test_exporter_with_filename_format(self): - self.test_exporter() + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + with override_settings(PAPERLESS_FILENAME_FORMAT="{created_year}/{correspondent}/{title}"): + self.test_exporter(use_filename_format=True) + + def test_update_export_changed_time(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + self._do_export() + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export() + m.assert_not_called() + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + Path(self.d1.source_path).touch() + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export() + self.assertEqual(m.call_count, 1) + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + def test_update_export_changed_checksum(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + self._do_export() + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export() + m.assert_not_called() + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + self.d2.checksum = "asdfasdgf3" + self.d2.save() + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export(compare_checksums=True) + self.assertEqual(m.call_count, 1) + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + @override_settings(PAPERLESS_FILENAME_FORMAT="{title}/{correspondent}") + def test_update_export_changed_location(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + m = self._do_export(use_filename_format=True) + self.assertTrue(os.path.isfile(os.path.join(self.target, "wow1", "c.pdf"))) + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + self.d1.title = "new_title" + self.d1.save() + self._do_export(use_filename_format=True) + self.assertFalse(os.path.isfile(os.path.join(self.target, "wow1", "c.pdf"))) + self.assertFalse(os.path.isdir(os.path.join(self.target, "wow1"))) + self.assertTrue(os.path.isfile(os.path.join(self.target, "new_title", "c.pdf"))) + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) def test_export_missing_files(self):