Skip to content

NXDRIVE-3182: [POC] Drive for Alfresco#6378

Open
gitofanindya wants to merge 5 commits into
masterfrom
wip-NXDRIVE-3182-poc-drive-for-alfresco
Open

NXDRIVE-3182: [POC] Drive for Alfresco#6378
gitofanindya wants to merge 5 commits into
masterfrom
wip-NXDRIVE-3182-poc-drive-for-alfresco

Conversation

@gitofanindya

@gitofanindya gitofanindya commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Add initial Alfresco-specific sync engine and routing so Drive can connect to Alfresco servers alongside Nuxeo.

New Features:

  • Introduce an AlfrescoEngine implementation that plugs into the existing engine infrastructure for account binding and synchronization against Alfresco Content Services.
  • Add an AlfrescoRemote client wrapper around the alfresco SDK to provide the remote operations needed by the sync engine.
  • Implement an AlfrescoRemoteWatcher to poll the Alfresco Sync Service for remote changes and feed them into the sync queue.
  • Automatically detect server type (Nuxeo vs Alfresco) from the server URL and bind using the appropriate engine.

Bug Fixes:

  • Map Alfresco authentication failures to the UNAUTHORIZED error code in the GUI when binding a server.

Enhancements:

  • Extend engine registration and GUI binding logic to support multiple engine types keyed by server type constants.
  • Persist Alfresco-specific sync metadata (subscriber, subscription, and since-marker cursors) in the engine DAO for incremental change polling.

Copilot AI review requested due to automatic review settings May 15, 2026 00:25
@sourcery-ai

sourcery-ai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a first-phase Alfresco backend to Nuxeo Drive by introducing an Alfresco-specific engine, remote client, and remote watcher, wiring them into server binding via URL-based engine-type detection and improving GUI error handling for Alfresco authentication failures.

Sequence diagram for Alfresco-aware server binding and auth handling

sequenceDiagram
    actor User
    participant GuiApi as GuiApi
    participant Manager as Manager
    participant AlfrescoEngine as AlfrescoEngine
    participant AlfrescoRemote as AlfrescoRemote

    User->>GuiApi: bind_server(url, username, password)
    GuiApi->>Manager: _detect_server_type(url)
    Manager-->>GuiApi: engine_type (NXDRIVE|ALFRESCO)
    GuiApi->>Manager: bind_engine(engine_type, local_folder, name, binder, starts)
    Manager->>AlfrescoEngine: __init__(manager, definition, binder)
    AlfrescoEngine->>AlfrescoEngine: bind(binder)
    AlfrescoEngine->>AlfrescoRemote: init_remote()
    AlfrescoEngine->>AlfrescoRemote: check_credentials()
    AlfrescoRemote-->>AlfrescoEngine: person
    AlfrescoEngine-->>Manager: engine instance
    Manager-->>GuiApi: engine

    rect rgb(255,230,230)
        AlfrescoRemote-->>AlfrescoEngine: AlfrescoAuthError
        AlfrescoEngine-->>GuiApi: raise AuthenticationError
        GuiApi-->>GuiApi: except AlfrescoAuthError
        GuiApi->>GuiApi: error = UNAUTHORIZED
    end

    GuiApi->>GuiApi: setMessage.emit(error, "error")
Loading

File-Level Changes

Change Details Files
Introduce Alfresco-specific sync engine wired into manager engine types and server binding flow.
  • Register a new AlfrescoEngine type in Manager._engine_types alongside the existing NXDRIVE engine.
  • Add Manager._detect_server_type() to infer engine type from URL suffix and use it in manager.bind_server() and GUI _bind_server() to select the engine.
  • Implement AlfrescoEngine subclass to replicate Engine initialization while using AlfrescoRemote, AlfrescoRemoteWatcher, and Alfresco-specific root setup and sync-service subscription handling.
nxdrive/manager.py
nxdrive/gui/api.py
nxdrive/constants.py
nxdrive/engine/alfresco_engine.py
Implement AlfrescoRemote client wrapper integrating the official Alfresco Python SDK with Drive abstractions.
  • Create AlfrescoRemote that wraps alfresco.Alfresco, building auth (OAuth2, bearer token, ticket, or basic) and configuring HTTP session headers.
  • Expose node CRUD, upload/download, search, and root introspection operations mapped to Drive’s RemoteFileInfo for use by AlfrescoEngine and processors.
  • Add sync-service integration methods to register a subscriber, subscribe a folder, fetch changes, and push sync payloads while persisting subscriber/subscription IDs via the DAO.
nxdrive/client/alfresco_remote.py
Add Alfresco-specific remote watcher that polls Alfresco Sync Service and maps changes into Drive’s local state model.
  • Implement AlfrescoRemoteWatcher EngineWorker that periodically calls AlfrescoRemote.get_changes() using a persisted since-marker and emits standard watcher signals.
  • Translate sync-service change entries into RemoteFileInfo objects and DocPair updates (insert, update, mark remote deletions) via EngineDAO.
  • Track and persist the last since-marker and basic polling metrics for observability.
nxdrive/engine/watcher/alfresco_remote_watcher.py
Improve GUI error handling to surface Alfresco authentication failures as UNAUTHORIZED instead of generic errors.
  • Update GUI bind_server() to detect AlfrescoAuthError exceptions and map them to the UNAUTHORIZED error code, preserving existing handling for other exception types.
  • Ensure URL-based engine-type detection is used consistently in the GUI when binding servers so Alfresco accounts route to AlfrescoEngine.
nxdrive/gui/api.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • AlfrescoEngine.init reimplements a large portion of Engine.init, which risks divergence over time; consider extracting the common initialization steps into a shared helper or making Engine more configurable (e.g., injectable remote_cls) so AlfrescoEngine can delegate instead of duplicating.
  • AlfrescoRemote.init accepts proxy, verify, and cert parameters but does not pass them to the underlying Alfresco client or otherwise use them; either wire these through so they take effect or drop them from the signature to avoid confusion.
  • AlfrescoRemoteWatcher._execute uses an unconditional loop (while "working":) and has some redundant first_pass logic in _handle_changes; making the loop condition explicit (e.g., based on a running flag) and simplifying the first_pass handling would make the control flow clearer and less error-prone.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- AlfrescoEngine.__init__ reimplements a large portion of Engine.__init__, which risks divergence over time; consider extracting the common initialization steps into a shared helper or making Engine more configurable (e.g., injectable remote_cls) so AlfrescoEngine can delegate instead of duplicating.
- AlfrescoRemote.__init__ accepts proxy, verify, and cert parameters but does not pass them to the underlying Alfresco client or otherwise use them; either wire these through so they take effect or drop them from the signature to avoid confusion.
- AlfrescoRemoteWatcher._execute uses an unconditional loop (`while "working":`) and has some redundant first_pass logic in _handle_changes; making the loop condition explicit (e.g., based on a running flag) and simplifying the first_pass handling would make the control flow clearer and less error-prone.

## Individual Comments

### Comment 1
<location path="nxdrive/client/alfresco_remote.py" line_range="47-53" />
<code_context>
+        *,
+        password: str = "",
+        token: Any = None,
+        proxy: "Proxy" = None,
+        download_callback: Callable = None,
+        upload_callback: Callable = None,
+        dao: "EngineDAO" = None,
+        timeout: int = Options.timeout,
+        verify: bool = True,
+        cert: Tuple[str] = None,
+        sync_service_url: Optional[str] = None,
+    ) -> None:
</code_context>
<issue_to_address>
**suggestion:** Either wire through or drop currently-unused constructor parameters like `proxy`, callbacks, and TLS options.

These parameters (`proxy`, `download_callback`, `upload_callback`, `verify`, `cert`) are accepted but never used, which can mislead callers into assuming proxy/TLS settings and callbacks are active. Please either wire them into the underlying `Alfresco` client (e.g., via its session/adapters) or remove them for now and reintroduce when they’re actually supported. If they must stay for interface compatibility, add targeted TODOs at the points where they should eventually be applied so they don’t remain silent no-ops.

Suggested implementation:

```python
        self.server_url = url
        self.user_id = user_id
        self.device_id = device_id
        self.version = version
        self.timeout = timeout if timeout > 0 else 30

        if dao:
            self.dao = dao

        # Store optional network/TLS and callback parameters so they are not
        # silently ignored. They are currently not wired into the underlying
        # Alfresco client and remain no-ops.
        self.proxy = proxy
        self.download_callback = download_callback
        self.upload_callback = upload_callback
        self.verify = verify
        self.cert = cert

        # TODO: Wire `proxy`, `verify`, and `cert` into the underlying HTTP session
        # or Alfresco client adapters when backend support is implemented.
        # TODO: Invoke `download_callback` and `upload_callback` at the appropriate
        # points in download/upload workflows so callers can observe transfer progress.

        # Build the authentication handler

```

If/when you locate the code that constructs the underlying `Alfresco` client or HTTP session in this class, add concrete usage of:
- `self.proxy`, `self.verify`, and `self.cert` into the session/adapters (e.g., `requests.Session(proxies=..., verify=..., cert=...)`).
- `self.download_callback` and `self.upload_callback` in the download/upload methods, calling them with relevant progress information.

At that point, you can remove or adjust the TODOs above to reference the actual implementation.
</issue_to_address>

### Comment 2
<location path="nxdrive/engine/watcher/alfresco_remote_watcher.py" line_range="80-82" />
<code_context>
+        if not remote:
+            return False
+
+        if first_pass:
+            self.initiate.emit()
+            if not first_pass:
+                return True
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Fix the first-pass logic in `_handle_changes`, which currently contains a dead check and can emit `initiate` twice.

The inner `if not first_pass:` is unreachable, and when there are no changes the later `if not changes:` block also emits `self.initiate.emit()` on the first pass, so `initiate` can fire twice on the initial empty cycle.

Given `_execute()` already sets `first_pass = False` when `_handle_changes()` returns `True`, you can simplify to:

```python
if first_pass:
    self.initiate.emit()

...
if not changes:
    self.empty_polls += 1
    self.noChangesFound.emit()
    self.updated.emit()
    return True
```

This removes the dead branch and prevents duplicate `initiate` emissions.
</issue_to_address>

### Comment 3
<location path="nxdrive/engine/alfresco_engine.py" line_range="65-74" />
<code_context>
+        # We must NOT call Engine.__init__ because it hardcodes Remote as the
</code_context>
<issue_to_address>
**suggestion:** Re-evaluate duplicating large parts of `Engine.__init__` to reduce divergence risk.

Reimplementing most of `Engine.__init__` to avoid the default `Remote` type creates a long‑term maintenance risk: any change to the base initialization must be manually mirrored.

Prefer refactoring `Engine` so that either:
* `remote_cls` / `local_cls` are parameters to `Engine.__init__`, or
* the shared initialization logic lives in a helper that `AlfrescoEngine` can reuse.

If refactoring isn’t feasible now, consider adding tests or checks that validate `AlfrescoEngine`’s initialized state against `Engine` (attributes, signal wiring, etc.) to detect drift early.

Suggested implementation:

```python
        self,
        manager: "Manager",
        definition: EngineDef,
        /,
        *,
        binder: Binder = None,
        processors: int = 10,
        remote_cls: Type[AlfrescoRemote] = AlfrescoRemote,
        local_cls: Type[LocalClientMixin] = LocalClient,
    ) -> None:
        # Delegate initialization to Engine.__init__, providing the
        # Alfresco-specific remote/local classes explicitly to avoid
        # divergence from the base Engine initialization.
        super().__init__(
            manager,
            definition,
            binder=binder,
            processors=processors,
            remote_cls=remote_cls,
            local_cls=local_cls,
        )

```

To fully implement the suggested refactor and make this work, you will also need to:

1. Update the base `Engine` class (likely in `nxdrive/engine/engine.py`):
   - Change `__init__` to accept `remote_cls` and `local_cls` keyword parameters with appropriate defaults, instead of hardcoding `Remote` (and the current local client) inside the method body. For example:
     - Add parameters: `remote_cls: Type[Remote] = Remote`, `local_cls: Type[LocalClientMixin] = LocalClient`.
     - Store them on `self` if needed and use them wherever the hardcoded `Remote`/local client are currently instantiated.
   - Ensure that all existing callers of `Engine(...)` still work by relying on those defaults (no call-site changes required for non-Alfresco engines).

2. Remove any Alfresco-specific initialization logic that is still duplicated in `AlfrescoEngine.__init__` but is now handled by `Engine.__init__` (if there are additional lines below the snippet you provided that mirror base Engine setup).

3. Optionally, to guard against future divergence:
   - Add tests that construct a plain `Engine` and an `AlfrescoEngine` and assert that their common initialization state (attributes, signal connections, etc.) is consistent except for the expected differences in `remote_cls`/`local_cls`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +47 to +53
proxy: "Proxy" = None,
download_callback: Callable = None,
upload_callback: Callable = None,
dao: "EngineDAO" = None,
timeout: int = Options.timeout,
verify: bool = True,
cert: Tuple[str] = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Either wire through or drop currently-unused constructor parameters like proxy, callbacks, and TLS options.

These parameters (proxy, download_callback, upload_callback, verify, cert) are accepted but never used, which can mislead callers into assuming proxy/TLS settings and callbacks are active. Please either wire them into the underlying Alfresco client (e.g., via its session/adapters) or remove them for now and reintroduce when they’re actually supported. If they must stay for interface compatibility, add targeted TODOs at the points where they should eventually be applied so they don’t remain silent no-ops.

Suggested implementation:

        self.server_url = url
        self.user_id = user_id
        self.device_id = device_id
        self.version = version
        self.timeout = timeout if timeout > 0 else 30

        if dao:
            self.dao = dao

        # Store optional network/TLS and callback parameters so they are not
        # silently ignored. They are currently not wired into the underlying
        # Alfresco client and remain no-ops.
        self.proxy = proxy
        self.download_callback = download_callback
        self.upload_callback = upload_callback
        self.verify = verify
        self.cert = cert

        # TODO: Wire `proxy`, `verify`, and `cert` into the underlying HTTP session
        # or Alfresco client adapters when backend support is implemented.
        # TODO: Invoke `download_callback` and `upload_callback` at the appropriate
        # points in download/upload workflows so callers can observe transfer progress.

        # Build the authentication handler

If/when you locate the code that constructs the underlying Alfresco client or HTTP session in this class, add concrete usage of:

  • self.proxy, self.verify, and self.cert into the session/adapters (e.g., requests.Session(proxies=..., verify=..., cert=...)).
  • self.download_callback and self.upload_callback in the download/upload methods, calling them with relevant progress information.

At that point, you can remove or adjust the TODOs above to reference the actual implementation.

Comment thread nxdrive/engine/watcher/alfresco_remote_watcher.py Outdated
Comment on lines +65 to +74
# We must NOT call Engine.__init__ because it hardcodes Remote as the
# remote_cls default. Instead, we replicate the relevant init steps.
QObject.__init__(self)

self.version = manager.version
self.remote: Optional[AlfrescoRemote] = None # type: ignore[assignment]
self._remote_token: Any = None

self.remote_cls = remote_cls
self.local_cls = local_cls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Re-evaluate duplicating large parts of Engine.__init__ to reduce divergence risk.

Reimplementing most of Engine.__init__ to avoid the default Remote type creates a long‑term maintenance risk: any change to the base initialization must be manually mirrored.

Prefer refactoring Engine so that either:

  • remote_cls / local_cls are parameters to Engine.__init__, or
  • the shared initialization logic lives in a helper that AlfrescoEngine can reuse.

If refactoring isn’t feasible now, consider adding tests or checks that validate AlfrescoEngine’s initialized state against Engine (attributes, signal wiring, etc.) to detect drift early.

Suggested implementation:

        self,
        manager: "Manager",
        definition: EngineDef,
        /,
        *,
        binder: Binder = None,
        processors: int = 10,
        remote_cls: Type[AlfrescoRemote] = AlfrescoRemote,
        local_cls: Type[LocalClientMixin] = LocalClient,
    ) -> None:
        # Delegate initialization to Engine.__init__, providing the
        # Alfresco-specific remote/local classes explicitly to avoid
        # divergence from the base Engine initialization.
        super().__init__(
            manager,
            definition,
            binder=binder,
            processors=processors,
            remote_cls=remote_cls,
            local_cls=local_cls,
        )

To fully implement the suggested refactor and make this work, you will also need to:

  1. Update the base Engine class (likely in nxdrive/engine/engine.py):

    • Change __init__ to accept remote_cls and local_cls keyword parameters with appropriate defaults, instead of hardcoding Remote (and the current local client) inside the method body. For example:
      • Add parameters: remote_cls: Type[Remote] = Remote, local_cls: Type[LocalClientMixin] = LocalClient.
      • Store them on self if needed and use them wherever the hardcoded Remote/local client are currently instantiated.
    • Ensure that all existing callers of Engine(...) still work by relying on those defaults (no call-site changes required for non-Alfresco engines).
  2. Remove any Alfresco-specific initialization logic that is still duplicated in AlfrescoEngine.__init__ but is now handled by Engine.__init__ (if there are additional lines below the snippet you provided that mirror base Engine setup).

  3. Optionally, to guard against future divergence:

    • Add tests that construct a plain Engine and an AlfrescoEngine and assert that their common initialization state (attributes, signal connections, etc.) is consistent except for the expected differences in remote_cls/local_cls.

@codecov

codecov Bot commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 4.83516% with 866 lines in your changes missing coverage. Please review.
✅ Project coverage is 18.30%. Comparing base (bc1094d) to head (4c2ccf7).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
nxdrive/engine/alfresco_engine.py 2.05% 238 Missing ⚠️
nxdrive/client/alfresco_remote.py 0.00% 218 Missing ⚠️
nxdrive/engine/watcher/alfresco_remote_watcher.py 0.00% 202 Missing ⚠️
nxdrive/auth/alfresco_oauth2.py 15.26% 111 Missing ⚠️
nxdrive/gui/api.py 15.95% 79 Missing ⚠️
nxdrive/manager.py 11.11% 8 Missing ⚠️
nxdrive/engine/processor.py 0.00% 5 Missing ⚠️
nxdrive/auth/__init__.py 40.00% 3 Missing ⚠️
nxdrive/engine/tracker.py 0.00% 1 Missing ⚠️
nxdrive/updater/base.py 0.00% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (bc1094d) and HEAD (4c2ccf7). Click for more details.

HEAD has 9 uploads less than BASE
Flag BASE (bc1094d) HEAD (4c2ccf7)
unit 3 0
functional 6 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #6378       +/-   ##
===========================================
- Coverage   86.53%   18.30%   -68.24%     
===========================================
  Files          98      103        +5     
  Lines       17716    19570     +1854     
===========================================
- Hits        15331     3582    -11749     
- Misses       2385    15988    +13603     
Flag Coverage Δ
functional ?
integration 18.30% <4.83%> (-10.57%) ⬇️
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread nxdrive/engine/watcher/alfresco_remote_watcher.py Fixed
Comment thread nxdrive/client/alfresco_remote.py Fixed
Comment thread nxdrive/client/alfresco_remote.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a proof-of-concept “Alfresco” backend alongside the existing Nuxeo engine, wiring in a new engine type, remote client, and remote watcher, and selecting the engine type during account binding based on the server URL.

Changes:

  • Add a new AlfrescoEngine plus an AlfrescoRemote client and AlfrescoRemoteWatcher.
  • Detect server type from the URL suffix and bind using the detected engine type (Manager + GUI API).
  • Add ALFRESCO_SERVER_TYPE constant and basic Alfresco-specific auth error mapping in the GUI binding flow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
nxdrive/manager.py Registers Alfresco engine type and auto-detects engine type during bind_server().
nxdrive/gui/api.py Uses Manager server-type detection during binding and maps Alfresco auth failures to UNAUTHORIZED.
nxdrive/engine/watcher/alfresco_remote_watcher.py New polling-based remote watcher consuming Alfresco sync-service change feed.
nxdrive/engine/alfresco_engine.py New Engine subclass for Alfresco binding/root setup and Alfresco watcher wiring.
nxdrive/constants.py Adds ALFRESCO_SERVER_TYPE.
nxdrive/client/alfresco_remote.py New remote client wrapper around the Alfresco SDK.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nxdrive/manager.py
from .dao.manager import ManagerDAO
from .direct_download import DirectDownload
from .direct_edit import DirectEdit
from .engine.alfresco_engine import AlfrescoEngine
Comment on lines +65 to +136
# We must NOT call Engine.__init__ because it hardcodes Remote as the
# remote_cls default. Instead, we replicate the relevant init steps.
QObject.__init__(self)

self.version = manager.version
self.remote: Optional[AlfrescoRemote] = None # type: ignore[assignment]
self._remote_token: Any = None

self.remote_cls = remote_cls
self.local_cls = local_cls
self.download_dir: Path = ROOT

self.doc_container_type = "Automatic"

self._threads: List[QThread] = []

self.invalidAuthentication.connect(self.stop)
self.timeout = Options.handshake_timeout
self.manager = manager

self.local_folder = Path(definition.local_folder)
self.folder = str(self.local_folder)
self.local = self.local_cls(
self.local_folder,
digest_callback=self.suspend_client,
download_dir=self.download_dir,
)

self.uid = definition.uid
self.name = definition.name
self._proc_count = processors
self._stopped = True
self._pause: bool = Options.debug
self._sync_started = False
self._invalid_credentials = False
self._offline_state = False
self.dao = EngineDAO(self._get_db_file())

self._remote_password: str = ""

if binder:
try:
self.bind(binder)
except Exception:
self.dispose_db()
raise

self._load_configuration()

self.download_dir = self._set_download_dir()
self.csv_dir = self._set_csv_dir_or_cleanup()

if not binder:
self._setup_local_folder(not Options.nofscheck)
if not self.server_url:
raise EngineInitError(self)
self.remote = self.init_remote()

self._create_queue_manager()
if Feature.synchronization:
self._create_remote_watcher()
self._create_local_watcher()

self.newQueueItem.connect(self._check_sync_start)
self.dao.newConflict.connect(self.conflict_resolver)

self._set_root_icon()
self._user_cache: Dict[str, str] = {}

self.noSpaceLeftOnDevice.connect(self.suspend)
self._threadpool = QThreadPool().globalInstance()

Comment on lines +198 to +206
# Save the configuration
self.dao.store_bool("web_authentication", self._web_authentication)
self.dao.update_config("server_url", self.server_url)
self.dao.update_config("remote_user", self.remote_user)
if self._remote_token:
self._save_token(self._remote_token)

# Establish the sync root
self._check_root()
Comment on lines +80 to +103
if first_pass:
self.initiate.emit()
if not first_pass:
return True

try:
changes_response = remote.get_changes(
since=self._since_marker, max_items=100
)
except Exception:
log.warning("Error fetching Alfresco changes", exc_info=True)
return first_pass

changes = changes_response.get("changes", [])
new_marker = changes_response.get("since", self._since_marker)

if not changes:
self.empty_polls += 1
self.noChangesFound.emit()
if first_pass:
# Even with no changes, we consider the first pass done
self.initiate.emit()
return True
self.updated.emit()
Comment on lines +144 to +148
self.dao.insert_remote_state(
remote_info,
parent_pair.remote_ref,
parent_pair.local_path,
parent_pair.local_path / name,
Comment on lines +37 to +60
def __init__(
self,
url: str,
user_id: str,
device_id: str,
version: str,
/,
*,
password: str = "",
token: Any = None,
proxy: "Proxy" = None,
download_callback: Callable = None,
upload_callback: Callable = None,
dao: "EngineDAO" = None,
timeout: int = Options.timeout,
verify: bool = True,
cert: Tuple[str] = None,
sync_service_url: Optional[str] = None,
) -> None:
self.server_url = url
self.user_id = user_id
self.device_id = device_id
self.version = version
self.timeout = timeout if timeout > 0 else 30
Comment thread nxdrive/manager.py
Comment on lines 828 to 853
def bind_server(
self,
local_folder: Path,
url: str,
username: str,
/,
*,
password: str = "",
token: Token = None,
name: str = None,
start_engine: bool = True,
check_credentials: bool = True,
) -> "Engine":
name = name or self._get_engine_name(url)
binder = Binder(
username=username,
password=password,
token=token,
no_check=not check_credentials,
no_fscheck=False,
url=url,
)
engine_type = self._detect_server_type(url)
return self.bind_engine(
DEFAULT_SERVER_TYPE, local_folder, name, binder, starts=start_engine
engine_type, local_folder, name, binder, starts=start_engine
)
Comment thread nxdrive/auth/alfresco_oauth2.py Fixed
Copilot AI review requested due to automatic review settings May 26, 2026 10:04
from .base import Authentication

if TYPE_CHECKING:
from ..dao.base import BaseDAO
Comment thread nxdrive/client/alfresco_remote.py Fixed
Comment thread nxdrive/client/alfresco_remote.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Comment thread nxdrive/manager.py
Comment on lines +37 to 38
from .engine.alfresco_engine import AlfrescoEngine
from .engine.engine import Engine
Comment thread nxdrive/manager.py

self._engine_types: Dict[str, Type[Engine]] = {"NXDRIVE": Engine}
self._engine_types: Dict[str, Type[Engine]] = {
"NXDRIVE": Engine,
Comment thread nxdrive/manager.py Outdated
everything else defaults to the Nuxeo engine.
"""
path = urlparse(url).path.rstrip("/")
if path.endswith("/alfresco") or path == "alfresco":
Comment on lines +389 to +393
self.dao.insert_remote_state(
remote_info,
parent_pair.remote_ref,
parent_pair.local_path,
parent_pair.local_path / name,
Comment on lines +39 to +57
def __init__(
self,
url: str,
user_id: str,
device_id: str,
version: str,
/,
*,
password: str = "",
token: Any = None,
proxy: "Proxy" = None,
download_callback: Callable = None,
upload_callback: Callable = None,
dao: "EngineDAO" = None,
timeout: int = Options.timeout,
verify: bool = True,
cert: Tuple[str] = None,
sync_service_url: Optional[str] = None,
) -> None:
Comment on lines +66 to +137
# We must NOT call Engine.__init__ because it hardcodes Remote as the
# remote_cls default. Instead, we replicate the relevant init steps.
QObject.__init__(self)

self.version = manager.version
self.remote: Optional[AlfrescoRemote] = None # type: ignore[assignment]
self._remote_token: Any = None

self.remote_cls = remote_cls
self.local_cls = local_cls
self.download_dir: Path = ROOT

self.doc_container_type = "Automatic"

self._threads: List[QThread] = []

self.invalidAuthentication.connect(self.stop)
self.timeout = Options.handshake_timeout
self.manager = manager

self.local_folder = Path(definition.local_folder)
self.folder = str(self.local_folder)
self.local = self.local_cls(
self.local_folder,
digest_callback=self.suspend_client,
download_dir=self.download_dir,
)

self.uid = definition.uid
self.name = definition.name
self._proc_count = processors
self._stopped = True
self._pause: bool = Options.debug
self._sync_started = False
self._invalid_credentials = False
self._offline_state = False
self.dao = EngineDAO(self._get_db_file())

self._remote_password: str = ""

if binder:
try:
self.bind(binder)
except Exception:
self.dispose_db()
raise

self._load_configuration()

self.download_dir = self._set_download_dir()
self.csv_dir = self._set_csv_dir_or_cleanup()

if not binder:
self._setup_local_folder(not Options.nofscheck)
if not self.server_url:
raise EngineInitError(self)
self.remote = self.init_remote()

self._create_queue_manager()
if Feature.synchronization:
self._create_remote_watcher()
self._create_local_watcher()

self.newQueueItem.connect(self._check_sync_start)
self.dao.newConflict.connect(self.conflict_resolver)

self._set_root_icon()
self._user_cache: Dict[str, str] = {}

self.noSpaceLeftOnDevice.connect(self.suspend)
self._threadpool = QThreadPool().globalInstance()

Comment thread nxdrive/engine/alfresco_engine.py Outdated
Comment on lines +314 to +352
import requests as _requests

parsed = urlparse(self.server_url)
candidates = [
# Standard standalone sync service on port 9090
urlunparse(
(
parsed.scheme,
f"{parsed.hostname}:9090",
"/alfresco",
"",
"",
"",
)
),
# Co-located: sync service on the same port as the repo
urlunparse(
(
parsed.scheme,
parsed.netloc,
"/alfresco",
"",
"",
"",
)
),
]

for candidate in candidates:
health_url = (
candidate.rstrip("/")
+ "/api/-default-/public/sync/versions/1/healthcheck"
)
try:
resp = _requests.get(health_url, timeout=2, verify=True)
if resp.ok:
log.info(f"Sync Service health-check passed at {candidate}")
return candidate
except Exception:
Comment on lines +45 to +55
url = (
server_url.rstrip("/")
+ "/api/-default-/private/alfresco/versions/1/config/syncServiceConfiguration"
)
try:
resp = requests.get(url, timeout=10, verify=verify)
resp.raise_for_status()
data = resp.json()
except Exception:
log.debug(f"Could not fetch syncServiceConfiguration from {url}", exc_info=True)
return {}
Comment thread nxdrive/auth/__init__.py
Comment on lines +18 to 26
When *server_type* is ``"ALFRESCO"`` and *token* is a dict (OAuth2),
an ``AlfrescoOAuthentication`` is returned so that the username is
resolved via the Alfresco People API instead of the Nuxeo Users API.
"""
server_type = kwargs.pop("server_type", None)
if isinstance(token, dict):
if server_type == "ALFRESCO":
return AlfrescoOAuthentication(host, token=token, **kwargs)
return OAuthentication(host, token=token, **kwargs)
token_url = oidc_resp.json().get("token_endpoint", "")
if token_url:
log.info(
f"Discovered token endpoint from {config_path}: {token_url}"
from ..utils import compute_digest

if TYPE_CHECKING:
from ..client.proxy import Proxy

if TYPE_CHECKING:
from ..client.proxy import Proxy
from ..dao.engine import EngineDAO
Comment thread nxdrive/gui/api.py
from urllib3.exceptions import LocationParseError

from ..auth import OAuthentication, Token, get_auth
from ..auth import AlfrescoOAuthentication, OAuthentication, Token, get_auth
Copilot AI review requested due to automatic review settings June 9, 2026 06:40
self.added_user_engine_list.remove(uid)
try:
self.added_user_engine_list.remove(uid)
except ValueError:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 11 comments.

Comment thread nxdrive/manager.py
Comment on lines 35 to 39
from .direct_download import DirectDownload
from .direct_edit import DirectEdit
from .engine.alfresco_engine import AlfrescoEngine
from .engine.engine import Engine
from .engine.tracker import Tracker
Comment thread nxdrive/manager.py
Comment on lines +162 to +165
self._engine_types: Dict[str, Type[Engine]] = {
"NXDRIVE": Engine,
ALFRESCO_SERVER_TYPE: AlfrescoEngine,
}
Comment thread nxdrive/manager.py
Comment on lines +863 to +866
path = urlparse(url).path.rstrip("/")
if path.endswith("/nuxeo") or path == "nuxeo":
return DEFAULT_SERVER_TYPE
return ALFRESCO_SERVER_TYPE
Comment thread nxdrive/manager.py
Comment on lines +857 to +861
def _detect_server_type(url: str, /) -> str:
"""Detect the server type from the URL suffix.

URLs ending with ``/nuxeo`` map to the Nuxeo engine;
everything else defaults to the Alfresco engine.
Comment thread nxdrive/gui/view.py
Comment on lines 1199 to +1203
if -1 < Options.feature_systray_history < len(files)
else len(files)
)
self.beginInsertRows(parent, 0, total_rows - 1)
self.files.extend(files)
self.endInsertRows()
if total_rows > 0:
self.beginInsertRows(parent, 0, total_rows - 1)
Comment thread nxdrive/gui/api.py
Comment on lines +989 to +997
try:
from alfresco.auth import TicketAuth

auth = TicketAuth(engine.remote_user, password, engine.server_url)
# Force ticket acquisition now so we fail fast on bad password
auth._obtain_ticket(engine.server_url)
ticket = auth.ticket
if not ticket:
raise RuntimeError("No ticket returned")
Comment on lines +92 to +95
base_url = url.rstrip("/")
if base_url.endswith("/alfresco"):
base_url = base_url[: -len("/alfresco")]

Comment on lines +565 to +567
url = self.server_url.rstrip("/") + "/api/discovery"
resp = self.client.session.get(url, timeout=self.timeout)
resp.raise_for_status()
Comment on lines +45 to +48
url = (
server_url.rstrip("/")
+ "/api/-default-/private/alfresco/versions/1/config/syncServiceConfiguration"
)
Comment on lines +207 to +210
url = (
self.url.rstrip("/")
+ "/api/-default-/public/alfresco/versions/1/people/-me-"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants