From 32e7cfea3d425469a98289f71db2dcfbdb231dc5 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Mon, 29 Apr 2024 05:24:21 +0200 Subject: [PATCH] fix(server): stacked assets for full sync, userIds as array for delta sync (#9100) * fix(server): stacked assets for full sync, userIds as array for delta sync * refactor(server): sync * fix getDeltaSync after partner removal --------- Co-authored-by: Jason Rasmussen --- mobile/openapi/.openapi-generator/FILES | 6 + mobile/openapi/README.md | Bin 26502 -> 26596 bytes mobile/openapi/doc/AssetDeltaSyncDto.md | Bin 0 -> 494 bytes mobile/openapi/doc/AssetFullSyncDto.md | Bin 0 -> 611 bytes mobile/openapi/doc/SyncApi.md | Bin 4954 -> 4454 bytes mobile/openapi/lib/api.dart | Bin 9313 -> 9392 bytes mobile/openapi/lib/api/sync_api.dart | Bin 4959 -> 3887 bytes mobile/openapi/lib/api_client.dart | Bin 25384 -> 25554 bytes .../lib/model/asset_delta_sync_dto.dart | Bin 0 -> 3258 bytes .../lib/model/asset_full_sync_dto.dart | Bin 0 -> 5233 bytes .../test/asset_delta_sync_dto_test.dart | Bin 0 -> 716 bytes .../test/asset_full_sync_dto_test.dart | Bin 0 -> 987 bytes mobile/openapi/test/sync_api_test.dart | Bin 850 -> 776 bytes open-api/immich-openapi-specs.json | 139 ++++++------ open-api/typescript-sdk/src/fetch-client.ts | 49 ++--- server/src/controllers/sync.controller.ts | 16 +- server/src/dtos/sync.dto.ts | 3 +- server/src/interfaces/asset.interface.ts | 2 + server/src/queries/asset.repository.sql | 202 ++++++++++++------ server/src/repositories/asset.repository.ts | 35 ++- server/src/services/sync.service.spec.ts | 13 +- server/src/services/sync.service.ts | 59 +++-- 22 files changed, 306 insertions(+), 218 deletions(-) create mode 100644 mobile/openapi/doc/AssetDeltaSyncDto.md create mode 100644 mobile/openapi/doc/AssetFullSyncDto.md create mode 100644 mobile/openapi/lib/model/asset_delta_sync_dto.dart create mode 100644 mobile/openapi/lib/model/asset_full_sync_dto.dart create mode 100644 mobile/openapi/test/asset_delta_sync_dto_test.dart create mode 100644 mobile/openapi/test/asset_full_sync_dto_test.dart diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index e7cbd570dc..4fb6bbdbb1 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -28,12 +28,14 @@ doc/AssetBulkUploadCheckDto.md doc/AssetBulkUploadCheckItem.md doc/AssetBulkUploadCheckResponseDto.md doc/AssetBulkUploadCheckResult.md +doc/AssetDeltaSyncDto.md doc/AssetDeltaSyncResponseDto.md doc/AssetFaceResponseDto.md doc/AssetFaceUpdateDto.md doc/AssetFaceUpdateItem.md doc/AssetFaceWithoutPersonResponseDto.md doc/AssetFileUploadResponseDto.md +doc/AssetFullSyncDto.md doc/AssetIdsDto.md doc/AssetIdsResponseDto.md doc/AssetJobName.md @@ -265,12 +267,14 @@ lib/model/asset_bulk_upload_check_dto.dart lib/model/asset_bulk_upload_check_item.dart lib/model/asset_bulk_upload_check_response_dto.dart lib/model/asset_bulk_upload_check_result.dart +lib/model/asset_delta_sync_dto.dart lib/model/asset_delta_sync_response_dto.dart lib/model/asset_face_response_dto.dart lib/model/asset_face_update_dto.dart lib/model/asset_face_update_item.dart lib/model/asset_face_without_person_response_dto.dart lib/model/asset_file_upload_response_dto.dart +lib/model/asset_full_sync_dto.dart lib/model/asset_ids_dto.dart lib/model/asset_ids_response_dto.dart lib/model/asset_job_name.dart @@ -449,12 +453,14 @@ test/asset_bulk_upload_check_dto_test.dart test/asset_bulk_upload_check_item_test.dart test/asset_bulk_upload_check_response_dto_test.dart test/asset_bulk_upload_check_result_test.dart +test/asset_delta_sync_dto_test.dart test/asset_delta_sync_response_dto_test.dart test/asset_face_response_dto_test.dart test/asset_face_update_dto_test.dart test/asset_face_update_item_test.dart test/asset_face_without_person_response_dto_test.dart test/asset_file_upload_response_dto_test.dart +test/asset_full_sync_dto_test.dart test/asset_ids_dto_test.dart test/asset_ids_response_dto_test.dart test/asset_job_name_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a0872d6f97f5cdad7ee77418ca0b86d91e93cbfc..9b19236e7b6b5b2cb46e242d677bfe098b3ff9da 100644 GIT binary patch delta 177 zcmZoW&-mm#D6}}WNJ}eLBPBms zUmqmqSdgigo1&baT9O7-RSXnK%P%Sgs?t=bQP9!?8WWdd{Pu=Ddo?S&zthv;3-yQRQ(OZxRLcTNP$L=Q|zf=|oV%DXaY^ZpoDGrV$Ov^i9!QFXli)4&UnXxc#R# U#w*Y$XSJap7M}}$EzcQaA703r)c^nh literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/AssetFullSyncDto.md b/mobile/openapi/doc/AssetFullSyncDto.md new file mode 100644 index 0000000000000000000000000000000000000000..8635fee22215c9c76a1171bb9d0ec2962a3ba523 GIT binary patch literal 611 zcma)3O=|)%5WVMD1oof?cD=VJ+qEE6k!nw}VAzZ{G+!j+LFkX~ByOo~4>gy}o4ogC zCR1@Az{#K+>lK)0mJ;C8$(YUA^(}hFs)|2*sW`p?FB~3Z|1OWf_WQk(5t+8ZN93a+ ze>XTzvuh@d?lZMGts^}~)R5RpTHqgidk*<(A@xxQ)ZP(Vc_c=Cbtrj->iR#-gfKGU zV+5H!TZte9iFQKdc&jbpZu}KDR!!(N6MShxnvM@13ssatY%pqfEZ{FuTa5!xyGukQ zvN#1r1=XuVZEkBJixR{e>ayMLftJD1^JQKiH#%Y0jhklrGM}}}In7lm-=8cvEFV!H ie5}hF(}aJkXY0j{T3TuiP6ba>{;;?d{8fCy82bdA1HFF$ literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/SyncApi.md b/mobile/openapi/doc/SyncApi.md index 1b28e10c8cc1525d0b67465017b827ee6c0d5dd5..f750f7d4ba3dcc4b69e188049c82ee571fc8a9fa 100644 GIT binary patch delta 600 zcmcbm_DpHQjmgH0TFe3d!IR?|CBmY$w9->c+)8tDf-Ccq-SUe9Cpksg@)2vYds+im&_*o#apdcqRIk6-&KTkiaI6rT)JJ)?bj8NgSQ}D?w zF0lcd7?fIEke^qa3N*`30W%CW!0v$=0rR~Tx+u)^ND7htjwB59x}E}z4~-}cOMoE} n#E+(EGA~D~3%b21fdkV3bR{V2!QMj>g9X*(5Y89aLh1$pCji+A delta 1170 zcma)6L2DC17^NTyu28**AP8dv(vIC`Hfc*5Yap~%1L7e~Pces0X0{AWc9)&mLm`lJ zFU2x{!iy)bUi2UIRs^s94!@aA=q7H^T$1efzW2R(-<$ot@$Mtk`yojU?kMG&;()p_Dv6CR5cH(rsA4kP`SSGruiCW zLizKe<2V)Q1=^y~P7A%~QXRIXWU9+#9EFk{tEhzv8#Vzt259u zJlJQuEFgqh9j7Yc;rX44?69jCp01|Q&r3V+{$9^Yw-?U3Y_=Bb-StH>{k?RV*$??K zNdoUNo**L~{^hqh1I#VXp_V-^M)srFq`(&gOKqAu)Ig75P-fVSa|xN5HzQpE)Ys)F z`pd(c2OlDZD1+!Qs;?g1>hb-uo~-RXo}Hu}D3;8_CvEyZ=1WEK_(wW2c@jGN1G{{C zZ8DuL*k7Y~wpoV$R=S$z#}_+iH({@fw17Uw`5E>#VFL$mi7mUkK3L$VLLn8Q5nNma c+6HjlD;3mbDosM9E@I|5z@$Hye=WQA2i#bCBLDyZ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3e2f23024e8899ff0cec9cbf5623a5cc914b24f6..6752397884edaa07827a0fedbe517997e9a0f2ef 100644 GIT binary patch delta 35 rcmaFpvB7i0V$R8%IAk}^;FMIE6vG44Fn_+U>GRd!uW3aMWMy1 zMd6tx86G7i1)h0n`5Nd(KwJdV0Sws5^1Qy2S1`K>!$JsIj<^8QMYjYRSx6=+XcVOu umSz^Erf5!n%dTz&lY%;#urtBori-Qx#i!u#ged@eOlPwu=T#=AS}p(%jo$nK literal 4959 zcmeHKZBH9V5dQ98G0BJ6t}b?p`k`_F*8!7QN`Zn=R3YRnzCFC{dGE42j^iT!duMiU z&u45?(x#P~_5*PHGA}#x%*;c(U2C_YcQHJ9{HVt zCqj?De=!ef05205foiwruIWpF!wr`Z1nE|tbBNYzv! zG(t+6Yqt&s%AuOdE12amea9CR{`7GY6jD4e()r4I)|8*4QTm3wpo@ce>oH-?_~WA) zJy!09XYRv|#9o>nW#)qJ98ruogYOhh-XP`!on#Gqrhg?ty?D#YQzE!654z5is>>00 zAN|Q7Uaq}U5!^LFa9$i{IOs*SbRah}A^bV$j$%m&^J%k`g8+wI;Dpd)>i4tkK(R@k zgArkSEK;xNowOFtKh#u!y{#@#YQTs%bp}P@-{^%|yQ{CD_l}zt zfP*dnM(TXSMJ^qHJT4q~{59Wxmd&iC^vCR1**hIHoEiXzcvO6IL#u~HCCyjX%jF)qOagY6a8|7PNakpo!#LzMiG28EM>6`y_ zuke=`s1GNDr&VVB3Nq)owXg`HfWT%6Yl07`b2142}DqI5w5M^SBr=T z&g?-JOW5B79xUf>r*$pjGvYGR;84@*N zgP@QZg45hX#~qM{Jd4bYLK@1nr)&i8nFw~MgvcDeCj+F$Kan`f(+e#>wo2yMONit@#2j6uiU~tft|aX}flUnXkey#6e!+eip3Z#aLcUuV2{xiH$>A!Y z{P&Z%&!a0Y#7ny3mZ6h9LerG~XHq0yFH&Dp6U#~*!l@HkTp8(1YyXR^B*6P`DKAGE z%Cf3(2TvW!lSc63^NVb?7ngmQhJUGj<^Q;X0vAi5#toMhH)|?+GHpJl!eb8rTLTB* diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..c7f3ce618a741eb751c5551ab741c92afdededce GIT binary patch literal 3258 zcmbVOZExC05dO}um`JB$bkwx{aH_~b8y)0CuMJX@emEgyZR{m%&v@P4brDMQ-*0Bt zHg-(g+#vz>?z}(G%raLF%(&0WfI9$?queM)w|l|+PIS%BHfFFjq<`p zMt&)jLG?`R@HZC*|D7}zjT?JfJ=U3ZB6E>S(4pEWX;|H!be5`2+9=i89g4ZF$fDOT zqIfHe?e)-|1HA#A%S_}d#P4~p7pKBnxUg2bnM|FyK4kICX}IqJjIaV9r7KKkK|m(= zu<8h|L2-bw3{j)A9grP>DsymKWy2y*gpBb+ucnFPz*&oah(xUDn; zm`uUfHx_9?!3?XNZ{PeAs8dyX_Kd@4E3LH9MJrM$C8I>jeEy{nsd9%9`5;moloz>pr{rAo3E!AtVRv4;4?*(L1X%bmK|1 zQJF~LGcF-TI@0$K=bQ(y#LD!We*0@w>2NP}`jd-;0sb$ne*N2vmsMhqP5D?GR{Vk& zGu$HVmKGj}uK%rdKx7H{8Dc>>3tvTANDCXIccrlvFJZTBP@qr!xWbyPhmaL1GMi|; zSC*TwL5JMP4Nd~?CsV}8eH&3%L^xBNiX$99Tb&R+{6e>Z-*6C&Ly3zIt^x()2{<%r z5R9l=2WY{?B4I4*Rz1QTc%u6;LBpmSM1TSMPZQKJ>_lG8g@AZ5gh#N`LVs}PhYVRL zSvRgpQX`AuFK`}-+mzD zUswbBd*I)aP=stvs1hF@cAbu!LW({wku5Etd`a$k_EqsT5_Oti=ltU2Ob;(E<@y&7p!Z zq&FpPl@8(JQ+fiNY$;^H1~@-~6-%}9ivP$1O>@@wyd>{Gz!p{OHp0B|mBuHRN&8#C zUb!tUQ4`S(se{9FFNbR7XvU)n<>{_S=CEl`u!5L$qM4?oL3=0pF@&eJE81u>=X<{X z4}YFWji=D89D0pW@KY9HVr`r|V1xuyUAzXFhER3uxPBVe4D$qOH9h)j5Mu;4TUm3U zzOf?REM6T1B3&RoZYAKMYSAtAf`E6|%PYG1mIUxO*Gl;>(yi|*(|fK*`(omcruuB* K8}E4!C(XY$sUD&L literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..fba8d65381a1750e1206c3cf4b603e7b56056c5c GIT binary patch literal 5233 zcmeHL-*4MC5Pr{JaVd)0!Ca^9!%(Di+M-DYcTF-h>3|^!3`U}3cCzS^R1G7||Gw`? z(WWKm2Zrsf4{;>&?s)v}yW@$wyIZ@v^#05F>GNL?PYyr7KR-O6qv5B+9vz(0$>}*A zpB@a4{@jKbOTNp6af4U=m#?EO%N;@ia zc?~f)5#{Lhi%~Wa#%^t4IY)X-dL~OTQ+@os+1knqVJ*G4R=T6AD9#pTcHlJKY!U4O zfX~uZrnDrelykah%3YH3?q#wMh30IRtq{41vVAP$J{T#S(&d40vbR1p%)>y0s8&)_ zS7EFm@~Si0T8-uxEAFa2IfVI`H?A&Telh-L;z7`Grh(N&StM0}HU zC1nYr@_7j`#8n};=}eWGgg=t(OlF8c<~Ko1h}w9rrDfAVBP*(00w@btl3p7c47#URUuA}BjjCgIXs}PA`e&l(aw7KP%=ho%iIAMl z>Pyd_!88SdR{g~)t+Z`x4D%?5r*(TwDL?5S*5*=fvB=u{x}-1IUdBJQkYV;-ONSz7 zCm*=}4fg$}mS-~`$)(JswZbg;NX02RkycqR)@@cMUW)4^y`>kl*Fs>s9~au8pH>zE z3=O?uzDb(VXd6ip*f6_9Az30&C0U|{O&}F2ba?1`qI8-xxM)H!c-kE4c^rm$n85)W z3I<+zH)wK_({}JQeZ!ph9i#sLWygDYEM~t1r+Snx(Cw&fAAWwcx`daZD;TR%6!aBC zG@zFB&FvcxzZ#Qge4D@9A&}7=c~cjr)7)I=681N?AlrSXzqqWsga1RTU;q5#WkfP8 zeoL}I&k+0tQW^uGEKvw{%;#dXMi`Nt8K-rrnZqHXN9i#zs$J zkQ=89-?^c|9YdhQ9q zX36M!^07cOnf7QZX0eCpa`N_QL(1`BXD2k~(abRS%71V_d^$cyi&}fOzh(!tP8FH8 zEj=6xiIV5}&h~1xl)#%EP9-|Q*;L4fHp$B26V0EG;gaLq1vPPaC(lE^#|Qvh`l6#tS7K99YAJdI*NMGBlwD>;f+Yne=RpR5MX<-(%(`cU+cxodzdi1zFg^iRocP#a9xbsL_JceTR z36Ru)2N{HYtp~Xoi9UyD30*>@-ZD*B3ENw;E70K{QtaYBM=s;Pyg>rhEE+$7F!Pu=O5FDe7R49Z^}*O5-kJ>383;(!xRl z?ZL9fy!V?ovhqC3b6EVU%7Y)vyXDVCwJhLzb+ep9QNmqW!EITrt{=XMY*bz~82IA& z@cVH#mb$YpkZN5}tu}NDgB>C@iVXE+dHzxB-n+ni9ccdGCMrL$3iNx>LF^WLSv>}M zGleMd5ck__XCtF!r5kCeTTLO^tls)GS_%tuG6Gu-tNYL=RoEBjs4QvHc_WN|_nnlsj3vM{@qC~%c3k{e%I9RzbuR;Ue zFN#!BZ(TsOc0sMl;3bT9jMynOIH2X(OHG4!fe$jY`K`B6`H@w?pCbjKTRq6?b&xk> zh$0^1VS4LqWNledCx)~p44EDeni;Yfm2tKg7;m&ypFXH|qeCo;T}GIA-9G9bFP%rL zJ(cpDr>-Ygdl1IRWm;YU$u!jwWF#<;5qUWpbGx-|#jg0}sDnY1ou0v;0zlAfvf2RU zCR&8+J<{~-st{-&+~}v1B)$ik%-~rtOIDh(p#j0@$X^0#CTG*8$Uu%KIxc?zZxDQt zRs(P0;eW!zPFY9Ep93cW3Ek9HoyDGZ;qvA!2eN%OU3EuUt-6y8$TAojh~E}ZsFSo` nXEypyCe_`^T{3U_{0lzG?s-=7uY@CJwSygJAer-_JQaTddE?&=p}Hp5GO4rGYFcyEasdE5dooi1 delta 221 zcmeBRyTrEP7~|wLCh2;|oE*3OqR`^hBDd0V0Fv-bfpbfNHh88e z)M_du76UEeQUHRI)Z)nsOx%2^ZkTMqWH$L2qlPxR<&J43sYN;pKAFWOHV{kf6d=YH QPZnhIVyo4(=Bnia07*qmwg3PC diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e831c6f3e7..ec859d56e2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4958,31 +4958,19 @@ } }, "/sync/delta-sync": { - "get": { + "post": { "operationId": "getDeltaSync", - "parameters": [ - { - "name": "updatedAfter", - "required": true, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "userIds", - "required": true, - "in": "query", - "schema": { - "format": "uuid", - "type": "array", - "items": { - "type": "string" + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetDeltaSyncDto" } } - } - ], + }, + "required": true + }, "responses": { "200": { "content": { @@ -5012,55 +5000,19 @@ } }, "/sync/full-sync": { - "get": { - "operationId": "getAllForUserFullSync", - "parameters": [ - { - "name": "lastCreationDate", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" + "post": { + "operationId": "getFullSyncForUser", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetFullSyncDto" + } } }, - { - "name": "lastId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "limit", - "required": true, - "in": "query", - "schema": { - "minimum": 1, - "type": "integer" - } - }, - { - "name": "updatedUntil", - "required": true, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "userId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "required": true + }, "responses": { "200": { "content": { @@ -7023,6 +6975,26 @@ ], "type": "object" }, + "AssetDeltaSyncDto": { + "properties": { + "updatedAfter": { + "format": "date-time", + "type": "string" + }, + "userIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "updatedAfter", + "userIds" + ], + "type": "object" + }, "AssetDeltaSyncResponseDto": { "properties": { "deleted": { @@ -7175,6 +7147,35 @@ ], "type": "object" }, + "AssetFullSyncDto": { + "properties": { + "lastCreationDate": { + "format": "date-time", + "type": "string" + }, + "lastId": { + "format": "uuid", + "type": "string" + }, + "limit": { + "minimum": 1, + "type": "integer" + }, + "updatedUntil": { + "format": "date-time", + "type": "string" + }, + "userId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "limit", + "updatedUntil" + ], + "type": "object" + }, "AssetIdsDto": { "properties": { "assetIds": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 378f77c54a..92fc5cd59c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -836,11 +836,22 @@ export type AssetIdsResponseDto = { error?: Error2; success: boolean; }; +export type AssetDeltaSyncDto = { + updatedAfter: string; + userIds: string[]; +}; export type AssetDeltaSyncResponseDto = { deleted: string[]; needsFullSync: boolean; upserted: AssetResponseDto[]; }; +export type AssetFullSyncDto = { + lastCreationDate?: string; + lastId?: string; + limit: number; + updatedUntil: string; + userId?: string; +}; export type SystemConfigFFmpegDto = { accel: TranscodeHWAccel; acceptedAudioCodecs: AudioCodec[]; @@ -2372,39 +2383,29 @@ export function addSharedLinkAssets({ id, key, assetIdsDto }: { body: assetIdsDto }))); } -export function getDeltaSync({ updatedAfter, userIds }: { - updatedAfter: string; - userIds: string[]; +export function getDeltaSync({ assetDeltaSyncDto }: { + assetDeltaSyncDto: AssetDeltaSyncDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AssetDeltaSyncResponseDto; - }>(`/sync/delta-sync${QS.query(QS.explode({ - updatedAfter, - userIds - }))}`, { - ...opts - })); + }>("/sync/delta-sync", oazapfts.json({ + ...opts, + method: "POST", + body: assetDeltaSyncDto + }))); } -export function getAllForUserFullSync({ lastCreationDate, lastId, limit, updatedUntil, userId }: { - lastCreationDate?: string; - lastId?: string; - limit: number; - updatedUntil: string; - userId?: string; +export function getFullSyncForUser({ assetFullSyncDto }: { + assetFullSyncDto: AssetFullSyncDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AssetResponseDto[]; - }>(`/sync/full-sync${QS.query(QS.explode({ - lastCreationDate, - lastId, - limit, - updatedUntil, - userId - }))}`, { - ...opts - })); + }>("/sync/full-sync", oazapfts.json({ + ...opts, + method: "POST", + body: assetFullSyncDto + }))); } export function getConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ diff --git a/server/src/controllers/sync.controller.ts b/server/src/controllers/sync.controller.ts index c12d42df23..63757f73f3 100644 --- a/server/src/controllers/sync.controller.ts +++ b/server/src/controllers/sync.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -12,13 +12,15 @@ import { SyncService } from 'src/services/sync.service'; export class SyncController { constructor(private service: SyncService) {} - @Get('full-sync') - getAllForUserFullSync(@Auth() auth: AuthDto, @Query() dto: AssetFullSyncDto): Promise { - return this.service.getAllAssetsForUserFullSync(auth, dto); + @Post('full-sync') + @HttpCode(HttpStatus.OK) + getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise { + return this.service.getFullSync(auth, dto); } - @Get('delta-sync') - getDeltaSync(@Auth() auth: AuthDto, @Query() dto: AssetDeltaSyncDto): Promise { - return this.service.getChangesForDeltaSync(auth, dto); + @Post('delta-sync') + @HttpCode(HttpStatus.OK) + getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise { + return this.service.getDeltaSync(auth, dto); } } diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index a69062ec2d..1a02ba5ca0 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,5 +1,4 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; import { IsInt, IsPositive } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { ValidateDate, ValidateUUID } from 'src/validation'; @@ -16,7 +15,6 @@ export class AssetFullSyncDto { @IsInt() @IsPositive() - @Type(() => Number) @ApiProperty({ type: 'integer' }) limit!: number; @@ -27,6 +25,7 @@ export class AssetFullSyncDto { export class AssetDeltaSyncDto { @ValidateDate() updatedAfter!: Date; + @ValidateUUID({ each: true }) userIds!: string[]; } diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index fb6345df7c..cad83f09d4 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -134,6 +134,8 @@ export interface AssetFullSyncOptions { lastCreationDate?: Date; lastId?: string; updatedUntil: Date; + isArchived?: false; + withStacked?: true; limit: number; } diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 81dce80d0f..7d49fb18df 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -798,16 +798,47 @@ SELECT "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", - "stack"."primaryAssetId" AS "stack_primaryAssetId" + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId" FROM "assets" "asset" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) WHERE - "asset"."ownerId" = $1 + "asset"."isVisible" = true + AND "asset"."ownerId" IN ($1) AND ("asset"."fileCreatedAt", "asset"."id") < ($2, $3) AND "asset"."updatedAt" <= $4 - AND "asset"."isVisible" = true ORDER BY "asset"."fileCreatedAt" DESC, "asset"."id" DESC @@ -816,72 +847,105 @@ LIMIT -- AssetRepository.getChangedDeltaSync SELECT - "AssetEntity"."id" AS "AssetEntity_id", - "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", - "AssetEntity"."ownerId" AS "AssetEntity_ownerId", - "AssetEntity"."libraryId" AS "AssetEntity_libraryId", - "AssetEntity"."deviceId" AS "AssetEntity_deviceId", - "AssetEntity"."type" AS "AssetEntity_type", - "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", - "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", - "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", - "AssetEntity"."createdAt" AS "AssetEntity_createdAt", - "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", - "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", - "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", - "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", - "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", - "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", - "AssetEntity"."isArchived" AS "AssetEntity_isArchived", - "AssetEntity"."isExternal" AS "AssetEntity_isExternal", - "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", - "AssetEntity"."isOffline" AS "AssetEntity_isOffline", - "AssetEntity"."checksum" AS "AssetEntity_checksum", - "AssetEntity"."duration" AS "AssetEntity_duration", - "AssetEntity"."isVisible" AS "AssetEntity_isVisible", - "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", - "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", - "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", - "AssetEntity"."stackId" AS "AssetEntity_stackId", - "AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId", - "AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description", - "AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth", - "AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight", - "AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte", - "AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation", - "AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal", - "AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate", - "AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone", - "AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude", - "AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude", - "AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType", - "AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city", - "AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID", - "AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId", - "AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state", - "AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country", - "AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make", - "AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model", - "AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel", - "AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber", - "AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength", - "AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso", - "AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime", - "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", - "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", - "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", - "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps", - "AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id", - "AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId" + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isReadOnly" AS "asset_isReadOnly", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "exifInfo"."assetId" AS "exifInfo_assetId", + "exifInfo"."description" AS "exifInfo_description", + "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", + "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight", + "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte", + "exifInfo"."orientation" AS "exifInfo_orientation", + "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal", + "exifInfo"."modifyDate" AS "exifInfo_modifyDate", + "exifInfo"."timeZone" AS "exifInfo_timeZone", + "exifInfo"."latitude" AS "exifInfo_latitude", + "exifInfo"."longitude" AS "exifInfo_longitude", + "exifInfo"."projectionType" AS "exifInfo_projectionType", + "exifInfo"."city" AS "exifInfo_city", + "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID", + "exifInfo"."autoStackId" AS "exifInfo_autoStackId", + "exifInfo"."state" AS "exifInfo_state", + "exifInfo"."country" AS "exifInfo_country", + "exifInfo"."make" AS "exifInfo_make", + "exifInfo"."model" AS "exifInfo_model", + "exifInfo"."lensModel" AS "exifInfo_lensModel", + "exifInfo"."fNumber" AS "exifInfo_fNumber", + "exifInfo"."focalLength" AS "exifInfo_focalLength", + "exifInfo"."iso" AS "exifInfo_iso", + "exifInfo"."exposureTime" AS "exifInfo_exposureTime", + "exifInfo"."profileDescription" AS "exifInfo_profileDescription", + "exifInfo"."colorspace" AS "exifInfo_colorspace", + "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."fps" AS "exifInfo_fps", + "stack"."id" AS "stack_id", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId" FROM - "assets" "AssetEntity" - LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" - LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId" + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) WHERE - ( - ("AssetEntity"."ownerId" IN ($1)) - AND ("AssetEntity"."isVisible" = $2) - AND ("AssetEntity"."updatedAt" > $3) + "asset"."isVisible" = true + AND "asset"."ownerId" IN ($1) + AND ( + "stack"."primaryAssetId" = "asset"."id" + OR "asset"."stackId" IS NULL ) + AND "asset"."updatedAt" > $2 diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 6bbc8cad89..a961ab97d6 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -710,21 +710,23 @@ export class AssetRepository implements IAssetRepository { ], }) getAllForUserFullSync(options: AssetFullSyncOptions): Promise { - const { ownerId, lastCreationDate, lastId, updatedUntil, limit } = options; - const builder = this.repository - .createQueryBuilder('asset') - .leftJoinAndSelect('asset.exifInfo', 'exifInfo') - .leftJoinAndSelect('asset.stack', 'stack') - .where('asset.ownerId = :ownerId', { ownerId }); + const { ownerId, isArchived, withStacked, lastCreationDate, lastId, updatedUntil, limit } = options; + const builder = this.getBuilder({ + userIds: [ownerId], + exifInfo: true, + withStacked, + isArchived, + }); + if (lastCreationDate !== undefined && lastId !== undefined) { builder.andWhere('(asset.fileCreatedAt, asset.id) < (:lastCreationDate, :lastId)', { lastCreationDate, lastId, }); } + return builder .andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil }) - .andWhere('asset.isVisible = true') .orderBy('asset.fileCreatedAt', 'DESC') .addOrderBy('asset.id', 'DESC') .limit(limit) @@ -734,18 +736,11 @@ export class AssetRepository implements IAssetRepository { @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] }) getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise { - return this.repository.find({ - where: { - ownerId: In(options.userIds), - isVisible: true, - updatedAt: MoreThan(options.updatedAfter), - }, - relations: { - exifInfo: true, - stack: true, - }, - take: options.limit, - withDeleted: true, - }); + const builder = this.getBuilder({ userIds: options.userIds, exifInfo: true, withStacked: true }) + .andWhere({ updatedAt: MoreThan(options.updatedAfter) }) + .take(options.limit) + .withDeleted(); + + return builder.getMany(); } } diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts index 87205c08f1..9a7dbbc152 100644 --- a/server/src/services/sync.service.spec.ts +++ b/server/src/services/sync.service.spec.ts @@ -39,13 +39,12 @@ describe(SyncService.name, () => { describe('getAllAssetsForUserFullSync', () => { it('should return a list of all assets owned by the user', async () => { assetMock.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]); - await expect( - sut.getAllAssetsForUserFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate }), - ).resolves.toEqual([ + await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([ mapAsset(assetStub.external, mapAssetOpts), mapAsset(assetStub.hasEncodedVideo, mapAssetOpts), ]); expect(assetMock.getAllForUserFullSync).toHaveBeenCalledWith({ + withStacked: true, ownerId: authStub.user1.user.id, updatedUntil: untilDate, limit: 2, @@ -57,7 +56,7 @@ describe(SyncService.name, () => { it('should return a response requiring a full sync when partners are out of sync', async () => { partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]); await expect( - sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), + sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0); @@ -66,7 +65,7 @@ describe(SyncService.name, () => { it('should return a response requiring a full sync when last sync was too long ago', async () => { partnerMock.getAll.mockResolvedValue([]); await expect( - sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }), + sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0); @@ -78,7 +77,7 @@ describe(SyncService.name, () => { Array.from({ length: 10_000 }).fill(assetStub.image), ); await expect( - sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), + sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(1); expect(auditMock.getAfter).toHaveBeenCalledTimes(0); @@ -89,7 +88,7 @@ describe(SyncService.name, () => { assetMock.getChangedDeltaSync.mockResolvedValue([assetStub.image1]); auditMock.getAfter.mockResolvedValue([assetStub.external.id]); await expect( - sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), + sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: false, upserted: [mapAsset(assetStub.image1, mapAssetOpts)], diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index be11d36fa0..88a4e172a6 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,5 +1,4 @@ import { Inject } from '@nestjs/common'; -import _ from 'lodash'; import { DateTime } from 'luxon'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AccessCore, Permission } from 'src/cores/access.core'; @@ -11,6 +10,9 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { setIsEqual } from 'src/utils/set'; + +const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export class SyncService { private access: AccessCore; @@ -24,52 +26,69 @@ export class SyncService { this.access = AccessCore.create(accessRepository); } - async getAllAssetsForUserFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { + async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { + // mobile implementation is faster if this is a single id const userId = dto.userId || auth.user.id; await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); const assets = await this.assetRepository.getAllForUserFullSync({ ownerId: userId, + // no archived assets for partner user + isArchived: userId === auth.user.id ? undefined : false, + // no stack for partner user + withStacked: userId === auth.user.id ? true : undefined, lastCreationDate: dto.lastCreationDate, updatedUntil: dto.updatedUntil, lastId: dto.lastId, limit: dto.limit, }); - const options = { auth, stripMetadata: false, withStack: true }; - return assets.map((a) => mapAsset(a, options)); + return assets.map((a) => mapAsset(a, { auth, stripMetadata: false, withStack: true })); } - async getChangesForDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise { - await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); - const partner = await this.partnerRepository.getAll(auth.user.id); - const userIds = [auth.user.id, ...partner.filter((p) => p.sharedWithId == auth.user.id).map((p) => p.sharedById)]; - userIds.sort(); - dto.userIds.sort(); + async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise { + // app has not synced in the last 100 days const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter)); - - if (!_.isEqual(userIds, dto.userIds) || duration > AUDIT_LOG_MAX_DURATION) { - // app does not have the correct partners synced - // or app has not synced in the last 100 days - return { needsFullSync: true, deleted: [], upserted: [] }; + if (duration > AUDIT_LOG_MAX_DURATION) { + return FULL_SYNC; } + const authUserId = auth.user.id; + + // app does not have the correct partners synced + const partner = await this.partnerRepository.getAll(authUserId); + const userIds = [authUserId, ...partner.filter((p) => p.sharedWithId == auth.user.id).map((p) => p.sharedById)]; + if (!setIsEqual(new Set(userIds), new Set(dto.userIds))) { + return FULL_SYNC; + } + + await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); + const limit = 10_000; const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); + // too many changes, need to do a full sync if (upserted.length === limit) { - // too many changes -> do a full sync (paginated) instead - return { needsFullSync: true, deleted: [], upserted: [] }; + return FULL_SYNC; } const deleted = await this.auditRepository.getAfter(dto.updatedAfter, { - userIds: userIds, + userIds, entityType: EntityType.ASSET, action: DatabaseAction.DELETE, }); - const options = { auth, stripMetadata: false, withStack: true }; const result = { needsFullSync: false, - upserted: upserted.map((a) => mapAsset(a, options)), + upserted: upserted + // do not return archived assets for partner users + .filter((a) => a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && !a.isArchived)) + .map((a) => + mapAsset(a, { + auth, + stripMetadata: false, + // ignore stacks for non partner users + withStack: a.ownerId === authUserId, + }), + ), deleted, }; return result;