From 1ad0deef2eed0799d0e312c1cd56396e3ee71a73 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9a=20Klein?= <lklein@nuxeo.com>
Date: Thu, 18 Oct 2018 18:54:04 +0200
Subject: [PATCH] NXDRIVE-1349: WIP Direct Edit on non-main blobs

---
 nxdrive/direct_edit.py | 117 +++++++++++++++++++++++++++--------------
 1 file changed, 77 insertions(+), 40 deletions(-)

diff --git a/nxdrive/direct_edit.py b/nxdrive/direct_edit.py
index 2b4b766e5..0bfcbc896 100644
--- a/nxdrive/direct_edit.py
+++ b/nxdrive/direct_edit.py
@@ -5,7 +5,7 @@
 from logging import getLogger
 from queue import Empty, Queue
 from time import sleep
-from typing import List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple
 from urllib.parse import quote
 
 from nuxeo.utils import get_digest_algorithm
@@ -147,7 +147,7 @@ def purge(path):
 
             ref = children[0].path
             try:
-                _, _, func, digest = self._extract_edit_info(ref)
+                _, _, func, digest, _ = self._extract_edit_info(ref)
             except NotFound:
                 # Engine is not known anymore
                 purge(child.path)
@@ -222,7 +222,12 @@ def _get_engine(
         return engine
 
     def _download(
-        self, engine: "Engine", info: NuxeoDocumentInfo, file_path: str, url: str = None
+        self,
+        engine: "Engine",
+        info: NuxeoDocumentInfo,
+        file_path: str,
+        url: str = None,
+        is_main_blob: bool = False,
     ) -> str:
         file_dir = os.path.dirname(file_path)
         file_name = os.path.basename(file_path)
@@ -232,7 +237,7 @@ def _download(
 
         # Close to processor method - should try to refactor ?
         pair = engine.get_dao().get_valid_duplicate_file(info.digest)
-        if pair:
+        if pair and is_main_blob:
             existing_file_path = engine.local.abspath(pair.local_path)
             log.debug(
                 "Local file matches remote digest %r, copying it from %r",
@@ -249,7 +254,6 @@ def _download(
                 engine.remote.download(
                     quote(url, safe="/:"),
                     file_out=file_out,
-                    digest=info.digest,
                     check_suspended=self.stop_client,
                 )
             else:
@@ -304,7 +308,31 @@ def _prepare_edit(
         if not info:
             return None
 
-        filename = info.filename
+        url = None
+        url_info: Dict[str, str] = {}
+        if download_url:
+            import re
+
+            urlmatch = re.match(
+                r"([^\/]+\/){3}(?P<xpath>.+)\/(?P<filename>[^\?]*).*",
+                download_url,
+                re.I,
+            )
+            if urlmatch:
+                url_info = urlmatch.groupdict()
+
+            url = server_url
+            if not url.endswith("/"):
+                url += "/"
+            url += download_url
+
+        xpath = url_info.get("xpath")
+
+        # If it is the main blob, we'll check if it's already synced.
+        # Otherwise, it's not a file that we sync so we have to download it.
+        is_main_blob = not download_url or xpath == "file:content"
+
+        filename = info.filename if is_main_blob else url_info.get("filename")
         self.directEditStarting.emit(engine.hostname, filename)
 
         # Create local structure
@@ -316,14 +344,9 @@ def _prepare_edit(
         file_path = os.path.join(dir_path, filename)
 
         # Download the file
-        url = None
-        if download_url:
-            url = server_url
-            if not url.endswith("/"):
-                url += "/"
-            url += download_url
-
-        tmp_file = self._download(engine, info, file_path, url=url)
+        tmp_file = self._download(
+            engine, info, file_path, url=url, is_main_blob=is_main_blob
+        )
         if tmp_file is None:
             log.error("Download failed")
             return None
@@ -336,7 +359,10 @@ def _prepare_edit(
         if user:
             self.local.set_remote_id(dir_path, user, name="nxdirectedituser")
 
-        if info.digest:
+        if xpath:
+            self.local.set_remote_id(dir_path, xpath, name="nxdirecteditxpath")
+
+        if is_main_blob and info.digest:
             self.local.set_remote_id(dir_path, info.digest, name="nxdirecteditdigest")
             # Set digest algorithm if not sent by the server
             digest_algorithm = info.digest_algorithm
@@ -383,7 +409,7 @@ def edit(
             else:
                 raise e
 
-    def _extract_edit_info(self, ref: str) -> Tuple[str, "Engine", str, str]:
+    def _extract_edit_info(self, ref: str) -> Tuple[str, "Engine", str, str, str]:
         dir_path = os.path.dirname(ref)
         server_url = self.local.get_remote_id(dir_path, name="nxdirectedit")
         user = self.local.get_remote_id(dir_path, name="nxdirectedituser")
@@ -395,8 +421,9 @@ def _extract_edit_info(self, ref: str) -> Tuple[str, "Engine", str, str]:
             dir_path, name="nxdirecteditdigestalgorithm"
         )
         digest = self.local.get_remote_id(dir_path, name="nxdirecteditdigest")
+        xpath = self.local.get_remote_id(dir_path, name="nxdirecteditxpath")
         uid = self.local.get_remote_id(dir_path)
-        return uid, engine, digest_algorithm, digest
+        return uid, engine, digest_algorithm, digest, xpath
 
     def force_update(self, ref: str, digest: str) -> None:
         dir_path = os.path.dirname(ref)
@@ -418,7 +445,7 @@ def _handle_lock_queue(self) -> None:
             dir_path = os.path.dirname(ref)
 
             try:
-                uid, engine, _, _ = self._extract_edit_info(ref)
+                uid, engine, _, _, _ = self._extract_edit_info(ref)
                 if action == "lock":
                     engine.remote.lock(uid)
                     self.local.set_remote_id(dir_path, b"1", name="nxdirecteditlock")
@@ -470,7 +497,7 @@ def _handle_upload_queue(self) -> None:
 
             log.trace("Handling DirectEdit queue ref: %r", ref)
 
-            uid, engine, algorithm, digest = self._extract_edit_info(ref)
+            uid, engine, algorithm, digest, xpath = self._extract_edit_info(ref)
             # Don't update if digest are the same
             try:
                 info = self.local.get_info(ref)
@@ -488,30 +515,40 @@ def _handle_upload_queue(self) -> None:
                     ref,
                 )
 
-                # TO_REVIEW Should check if server-side blob has changed ?
-                # Update the document, should verify
-                # the remote hash NXDRIVE-187
-                remote_info = engine.remote.get_info(uid)
-                if remote_info.digest != digest:
-                    # Conflict detect
-                    log.trace(
-                        "Remote digest: %s is different from "
-                        "the recorded  one: %s - conflict detected "
-                        "for %r",
-                        remote_info.digest,
-                        digest,
-                        ref,
-                    )
-                    self.directEditConflict.emit(
-                        os.path.basename(ref), ref, remote_info.digest
-                    )
-                    continue
+                if not xpath or xpath == "file:content":
+                    # TO_REVIEW Should check if server-side blob has changed ?
+                    # Update the document, should verify
+                    # the remote hash NXDRIVE-187
+                    remote_info = engine.remote.get_info(uid)
+                    if remote_info.digest != digest:
+                        # Conflict detect
+                        log.trace(
+                            "Remote digest: %s is different from "
+                            "the recorded  one: %s - conflict detected "
+                            "for %r",
+                            remote_info.digest,
+                            digest,
+                            ref,
+                        )
+                        self.directEditConflict.emit(
+                            os.path.basename(ref), ref, remote_info.digest
+                        )
+                        continue
 
                 os_path = self.local.abspath(ref)
                 log.debug("Uploading file %r", os_path)
-                engine.remote.stream_update(
-                    uid, os_path, fs=False, apply_versioning_policy=True
-                )
+                if not xpath or xpath == "file:content":
+                    engine.remote.stream_update(
+                        uid, os_path, fs=False, apply_versioning_policy=True
+                    )
+                else:
+                    engine.remote.upload(
+                        os_path,
+                        command="Blob.AttachOnDocument",
+                        document=engine.remote._check_ref(uid),
+                        applyVersioningPolicy=True,
+                        xpath=xpath,
+                    )
 
                 # Update hash value
                 dir_path = os.path.dirname(ref)
