NXDRIVE-3182: [POC] Drive for Alfresco#6378
Conversation
Reviewer's GuideAdds 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 handlingsequenceDiagram
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")
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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, |
There was a problem hiding this comment.
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 handlerIf/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, andself.certinto the session/adapters (e.g.,requests.Session(proxies=..., verify=..., cert=...)).self.download_callbackandself.upload_callbackin 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.
| # 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 |
There was a problem hiding this comment.
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_clsare parameters toEngine.__init__, or- the shared initialization logic lives in a helper that
AlfrescoEnginecan 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:
-
Update the base
Engineclass (likely innxdrive/engine/engine.py):- Change
__init__to acceptremote_clsandlocal_clskeyword parameters with appropriate defaults, instead of hardcodingRemote(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
selfif needed and use them wherever the hardcodedRemote/local client are currently instantiated.
- Add parameters:
- Ensure that all existing callers of
Engine(...)still work by relying on those defaults (no call-site changes required for non-Alfresco engines).
- Change
-
Remove any Alfresco-specific initialization logic that is still duplicated in
AlfrescoEngine.__init__but is now handled byEngine.__init__(if there are additional lines below the snippet you provided that mirror base Engine setup). -
Optionally, to guard against future divergence:
- Add tests that construct a plain
Engineand anAlfrescoEngineand assert that their common initialization state (attributes, signal connections, etc.) is consistent except for the expected differences inremote_cls/local_cls.
- Add tests that construct a plain
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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
AlfrescoEngineplus anAlfrescoRemoteclient andAlfrescoRemoteWatcher. - Detect server type from the URL suffix and bind using the detected engine type (Manager + GUI API).
- Add
ALFRESCO_SERVER_TYPEconstant 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.
| from .dao.manager import ManagerDAO | ||
| from .direct_download import DirectDownload | ||
| from .direct_edit import DirectEdit | ||
| from .engine.alfresco_engine import AlfrescoEngine |
| # 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() | ||
|
|
| # 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() |
| 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() |
| self.dao.insert_remote_state( | ||
| remote_info, | ||
| parent_pair.remote_ref, | ||
| parent_pair.local_path, | ||
| parent_pair.local_path / name, |
| 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 |
| 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 | ||
| ) |
| from .base import Authentication | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ..dao.base import BaseDAO |
| from .engine.alfresco_engine import AlfrescoEngine | ||
| from .engine.engine import Engine |
|
|
||
| self._engine_types: Dict[str, Type[Engine]] = {"NXDRIVE": Engine} | ||
| self._engine_types: Dict[str, Type[Engine]] = { | ||
| "NXDRIVE": Engine, |
| everything else defaults to the Nuxeo engine. | ||
| """ | ||
| path = urlparse(url).path.rstrip("/") | ||
| if path.endswith("/alfresco") or path == "alfresco": |
| self.dao.insert_remote_state( | ||
| remote_info, | ||
| parent_pair.remote_ref, | ||
| parent_pair.local_path, | ||
| parent_pair.local_path / name, |
| 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: |
| # 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() | ||
|
|
| 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: |
| 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 {} |
| 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 |
| from urllib3.exceptions import LocationParseError | ||
|
|
||
| from ..auth import OAuthentication, Token, get_auth | ||
| from ..auth import AlfrescoOAuthentication, OAuthentication, Token, get_auth |
| self.added_user_engine_list.remove(uid) | ||
| try: | ||
| self.added_user_engine_list.remove(uid) | ||
| except ValueError: |
| 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 |
| self._engine_types: Dict[str, Type[Engine]] = { | ||
| "NXDRIVE": Engine, | ||
| ALFRESCO_SERVER_TYPE: AlfrescoEngine, | ||
| } |
| path = urlparse(url).path.rstrip("/") | ||
| if path.endswith("/nuxeo") or path == "nuxeo": | ||
| return DEFAULT_SERVER_TYPE | ||
| return ALFRESCO_SERVER_TYPE |
| 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. |
| 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) |
| 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") |
| base_url = url.rstrip("/") | ||
| if base_url.endswith("/alfresco"): | ||
| base_url = base_url[: -len("/alfresco")] | ||
|
|
| url = self.server_url.rstrip("/") + "/api/discovery" | ||
| resp = self.client.session.get(url, timeout=self.timeout) | ||
| resp.raise_for_status() |
| url = ( | ||
| server_url.rstrip("/") | ||
| + "/api/-default-/private/alfresco/versions/1/config/syncServiceConfiguration" | ||
| ) |
| url = ( | ||
| self.url.rstrip("/") | ||
| + "/api/-default-/public/alfresco/versions/1/people/-me-" | ||
| ) |
Summary by Sourcery
Add initial Alfresco-specific sync engine and routing so Drive can connect to Alfresco servers alongside Nuxeo.
New Features:
Bug Fixes:
Enhancements: