overleaf-sync 2.0.0a2__tar.gz → 2.0.0a3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overleaf-sync
3
- Version: 2.0.0a2
3
+ Version: 2.0.0a3
4
4
  Summary: Overleaf Two-Way Sync Tool
5
5
  Keywords: overleaf,sync,latex,tex
6
6
  Author-email: Moritz Glöckl <moritzgloeckl@users.noreply.github.com>
@@ -78,11 +78,20 @@ Keep the `.olauth` file save, as it can be used to log in into your account.
78
78
  On a server without a display, or if the login window doesn't work for your account (e.g. single sign-on via ORCID or your institution), log in with a session cookie instead:
79
79
 
80
80
  1. Log in to Overleaf in your normal browser.
81
- 2. Open the developer tools → Application (Firefox: Storage) → Cookies → `https://www.overleaf.com` and copy the value of `overleaf_session2`.
81
+ 2. Open the developer tools → Application (Firefox: Storage) → Cookies → `https://www.overleaf.com` and copy the value of `overleaf_session2` (on a self-hosted server: the cookie `overleaf.sid` of your server).
82
82
  3. Run `ols login --cookie` and paste the value at the (hidden) prompt, or provide it via the `OLSYNC_COOKIE` environment variable.
83
83
 
84
84
  This works without installing the `gui` extra.
85
85
 
86
+ ### Self-hosted Overleaf servers
87
+ overleaf-sync also works with self-hosted Overleaf servers. It is tested against Overleaf Community Edition; Server Pro shares its code and should work too, but is untested. Pass the server's URL when logging in:
88
+
89
+ ```
90
+ moritz@github:~/test$ ols login --server https://overleaf.example.com
91
+ ```
92
+
93
+ The server is stored together with the login, so all other commands (`ols`, `ols list`, `ols download`) use it automatically. You can also set it with `--server` on any command or with the `OLSYNC_SERVER` environment variable. `ols login --cookie --server ...` works too; self-hosted servers name the session cookie `overleaf.sid` (`sharelatex.sid` before version 5).
94
+
86
95
  ### Listing all projects
87
96
  ```
88
97
  moritz@github:~/test$ ols list [--store-path -v/--verbose]
@@ -49,11 +49,20 @@ Keep the `.olauth` file save, as it can be used to log in into your account.
49
49
  On a server without a display, or if the login window doesn't work for your account (e.g. single sign-on via ORCID or your institution), log in with a session cookie instead:
50
50
 
51
51
  1. Log in to Overleaf in your normal browser.
52
- 2. Open the developer tools → Application (Firefox: Storage) → Cookies → `https://www.overleaf.com` and copy the value of `overleaf_session2`.
52
+ 2. Open the developer tools → Application (Firefox: Storage) → Cookies → `https://www.overleaf.com` and copy the value of `overleaf_session2` (on a self-hosted server: the cookie `overleaf.sid` of your server).
53
53
  3. Run `ols login --cookie` and paste the value at the (hidden) prompt, or provide it via the `OLSYNC_COOKIE` environment variable.
54
54
 
55
55
  This works without installing the `gui` extra.
56
56
 
57
+ ### Self-hosted Overleaf servers
58
+ overleaf-sync also works with self-hosted Overleaf servers. It is tested against Overleaf Community Edition; Server Pro shares its code and should work too, but is untested. Pass the server's URL when logging in:
59
+
60
+ ```
61
+ moritz@github:~/test$ ols login --server https://overleaf.example.com
62
+ ```
63
+
64
+ The server is stored together with the login, so all other commands (`ols`, `ols list`, `ols download`) use it automatically. You can also set it with `--server` on any command or with the `OLSYNC_SERVER` environment variable. `ols login --cookie --server ...` works too; self-hosted servers name the session cookie `overleaf.sid` (`sharelatex.sid` before version 5).
65
+
57
66
  ### Listing all projects
58
67
  ```
59
68
  moritz@github:~/test$ ols list [--store-path -v/--verbose]
@@ -1,3 +1,3 @@
1
1
  """Overleaf Two-Way Sync Tool"""
2
2
 
3
- __version__ = '2.0.0a2'
3
+ __version__ = '2.0.0a3'
@@ -13,13 +13,14 @@ from PySide6.QtWidgets import *
13
13
  from PySide6.QtWebEngineWidgets import *
14
14
  from PySide6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings, QWebEnginePage
15
15
 
16
- # Where to get the CSRF Token and where to send the login request to
17
- LOGIN_URL = "https://www.overleaf.com/login"
18
- PROJECT_URL = "https://www.overleaf.com/project" # The dashboard URL
16
+ BASE_URL = "https://www.overleaf.com" # The default Overleaf server
17
+ LOGIN_PATH = "/login" # Where the user logs in
18
+ PROJECT_PATH = "/project" # The dashboard, reached after a successful login
19
19
  # JS snippet to extract the csrfToken
20
20
  JAVASCRIPT_CSRF_EXTRACTOR = "document.getElementsByName('ol-csrfToken')[0].content"
21
- # Name of the cookies we want to extract
22
- COOKIE_NAMES = ["overleaf_session2", "GCLB"]
21
+ # Name of the cookies we want to extract: overleaf.com's session and load balancer cookies, and the session cookie of
22
+ # self-hosted servers (Community Edition; "sharelatex.sid" before version 5)
23
+ COOKIE_NAMES = ["overleaf_session2", "GCLB", "overleaf.sid", "sharelatex.sid"]
23
24
 
24
25
 
25
26
  class OlBrowserLoginWindow(QMainWindow):
@@ -28,9 +29,11 @@ class OlBrowserLoginWindow(QMainWindow):
28
29
  Opens a browser window to securely login the user and returns relevant login data.
29
30
  """
30
31
 
31
- def __init__(self, *args, **kwargs):
32
+ def __init__(self, base_url=BASE_URL, *args, **kwargs):
32
33
  super(OlBrowserLoginWindow, self).__init__(*args, **kwargs)
33
34
 
35
+ self._base_url = base_url.rstrip("/")
36
+
34
37
  self.webview = QWebEngineView()
35
38
 
36
39
  self._cookies = {}
@@ -46,7 +49,7 @@ class OlBrowserLoginWindow(QMainWindow):
46
49
 
47
50
  webpage = QWebEnginePage(self.profile, self)
48
51
  self.webview.setPage(webpage)
49
- self.webview.load(QUrl.fromUserInput(LOGIN_URL))
52
+ self.webview.load(QUrl.fromUserInput(self._base_url + LOGIN_PATH))
50
53
  self.webview.loadFinished.connect(self.handle_load_finished)
51
54
 
52
55
  self.setCentralWidget(self.webview)
@@ -58,7 +61,7 @@ class OlBrowserLoginWindow(QMainWindow):
58
61
  self._login_success = True
59
62
  QCoreApplication.quit()
60
63
 
61
- if self.webview.url().toString() == PROJECT_URL:
64
+ if self.webview.url().toString().rstrip("/") == self._base_url + PROJECT_PATH:
62
65
  self.webview.page().runJavaScript(
63
66
  JAVASCRIPT_CSRF_EXTRACTOR, 0, callback
64
67
  )
@@ -81,14 +84,14 @@ class OlBrowserLoginWindow(QMainWindow):
81
84
  return self._login_success
82
85
 
83
86
 
84
- def login():
87
+ def login(base_url=BASE_URL):
85
88
  from PySide6.QtCore import QLoggingCategory
86
89
  QLoggingCategory.setFilterRules('''\
87
90
  qt.webenginecontext.info=false
88
91
  ''')
89
92
 
90
93
  app = QApplication([])
91
- ol_browser_login_window = OlBrowserLoginWindow()
94
+ ol_browser_login_window = OlBrowserLoginWindow(base_url)
92
95
  ol_browser_login_window.show()
93
96
  app.exec()
94
97
 
@@ -24,18 +24,32 @@ import time
24
24
  if not hasattr(websocket, "SSLError"):
25
25
  websocket.SSLError = ssl.SSLError
26
26
 
27
- # Where to get the CSRF Token and where to send the login request to
28
- LOGIN_URL = "https://www.overleaf.com/login"
29
- PROJECT_URL = "https://www.overleaf.com/project" # The dashboard URL
30
- # The URL to download all the files in zip format
31
- DOWNLOAD_URL = "https://www.overleaf.com/project/{}/download/zip"
32
- UPLOAD_URL = "https://www.overleaf.com/project/{}/upload" # The URL to upload files
33
- FOLDER_URL = "https://www.overleaf.com/project/{}/folder" # The URL to create folders
34
- DELETE_URL = "https://www.overleaf.com/project/{}/{}/{}" # The URL to delete files (entity type: doc or file)
35
- COMPILE_URL = "https://www.overleaf.com/project/{}/compile?enable_pdf_caching=true" # The URL to compile the project
36
- BASE_URL = "https://www.overleaf.com" # The Overleaf Base URL
27
+ BASE_URL = "https://www.overleaf.com" # The default Overleaf server; self-hosted servers are passed as base_url
28
+ # Paths relative to the server's base URL
29
+ LOGIN_PATH = "/login" # Where to get the CSRF Token and where to send the login request to
30
+ PROJECT_PATH = "/project" # The dashboard
31
+ DOWNLOAD_PATH = "/project/{}/download/zip" # Download all the files in zip format
32
+ UPLOAD_PATH = "/project/{}/upload" # Upload files
33
+ FOLDER_PATH = "/project/{}/folder" # Create folders
34
+ DELETE_PATH = "/project/{}/{}/{}" # Delete files (entity type: doc or file)
35
+ COMPILE_PATH = "/project/{}/compile?enable_pdf_caching=true" # Compile the project
36
+ SOCKET_IO_PATH = "/socket.io/1/" # Real-time service handshake
37
37
  PATH_SEP = "/" # Use hardcoded path separator for both windows and posix system
38
38
 
39
+ class RealtimeSocket(SocketIO):
40
+ """
41
+ socketIO-client's SocketIO, minus its cleanup quirk: SocketIO.__del__ calls disconnect(), which opens a brand-new
42
+ connection when the old one is already closed (and, with wait_for_connection=True, retries that forever).
43
+ """
44
+
45
+ def __del__(self):
46
+ try:
47
+ if self.connected:
48
+ self.disconnect()
49
+ except Exception:
50
+ pass
51
+
52
+
39
53
  class OverleafClient(object):
40
54
  """
41
55
  Overleaf API Wrapper
@@ -51,9 +65,17 @@ class OverleafClient(object):
51
65
  if all(p.get(k) == v for k, v in more_attrs.items()):
52
66
  yield p
53
67
 
54
- def __init__(self, cookie=None, csrf=None):
68
+ def __init__(self, cookie=None, csrf=None, base_url=BASE_URL):
55
69
  self._cookie = cookie # Store the cookie for authenticated requests
56
70
  self._csrf = csrf # Store the CSRF token since it is needed for some requests
71
+ self._base_url = base_url.rstrip("/") # overleaf.com or a self-hosted server
72
+
73
+ @property
74
+ def base_url(self):
75
+ return self._base_url
76
+
77
+ def _url(self, path):
78
+ return self._base_url + path
57
79
 
58
80
  def login(self, username, password):
59
81
  """
@@ -63,7 +85,7 @@ class OverleafClient(object):
63
85
  Returns: Dict of cookie and CSRF
64
86
  """
65
87
 
66
- get_login = reqs.get(LOGIN_URL)
88
+ get_login = reqs.get(self._url(LOGIN_PATH))
67
89
  self._csrf = BeautifulSoup(get_login.content, 'html.parser').find(
68
90
  'input', {'name': '_csrf'}).get('value')
69
91
  login_json = {
@@ -71,7 +93,7 @@ class OverleafClient(object):
71
93
  "email": username,
72
94
  "password": password
73
95
  }
74
- post_login = reqs.post(LOGIN_URL, json=login_json,
96
+ post_login = reqs.post(self._url(LOGIN_PATH), json=login_json,
75
97
  cookies=get_login.cookies)
76
98
 
77
99
  # On a successful authentication the Overleaf API returns a new authenticated cookie.
@@ -84,7 +106,7 @@ class OverleafClient(object):
84
106
  self._cookie['GCLB'] = get_login.cookies['GCLB']
85
107
 
86
108
  # CSRF changes after making the login request, new CSRF token will be on the projects page
87
- projects_page = reqs.get(PROJECT_URL, cookies=self._cookie)
109
+ projects_page = reqs.get(self._url(PROJECT_PATH), cookies=self._cookie)
88
110
  self._csrf = BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-csrfToken'}) \
89
111
  .get('content')
90
112
 
@@ -114,7 +136,7 @@ class OverleafClient(object):
114
136
  Get the CSRF token from the project dashboard, e.g. after logging in with a cookie copied from a browser
115
137
  Returns: the CSRF token, or None if the session cookie is not (or no longer) valid
116
138
  """
117
- projects_page = reqs.get(PROJECT_URL, cookies=self._cookie, allow_redirects=False)
139
+ projects_page = reqs.get(self._url(PROJECT_PATH), cookies=self._cookie, allow_redirects=False)
118
140
  if not projects_page.ok or projects_page.is_redirect:
119
141
  return None
120
142
  csrf_tag = BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-csrfToken'})
@@ -127,7 +149,7 @@ class OverleafClient(object):
127
149
  Get all of a user's active projects (= not archived and not trashed)
128
150
  Returns: List of project objects
129
151
  """
130
- projects_page = reqs.get(PROJECT_URL, cookies=self._cookie)
152
+ projects_page = reqs.get(self._url(PROJECT_PATH), cookies=self._cookie)
131
153
  json_content = OverleafClient._extract_projects(projects_page.content)
132
154
  return list(OverleafClient.filter_projects(json_content))
133
155
 
@@ -138,7 +160,7 @@ class OverleafClient(object):
138
160
  Returns: project object
139
161
  """
140
162
 
141
- projects_page = reqs.get(PROJECT_URL, cookies=self._cookie)
163
+ projects_page = reqs.get(self._url(PROJECT_PATH), cookies=self._cookie)
142
164
  json_content = OverleafClient._extract_projects(projects_page.content)
143
165
  return next(OverleafClient.filter_projects(json_content, {"name": project_name}), None)
144
166
 
@@ -148,7 +170,7 @@ class OverleafClient(object):
148
170
  Params: project_id, the id of the project
149
171
  Returns: bytes string (zip file)
150
172
  """
151
- r = reqs.get(DOWNLOAD_URL.format(project_id),
173
+ r = reqs.get(self._url(DOWNLOAD_PATH.format(project_id)),
152
174
  stream=True, cookies=self._cookie)
153
175
  return r.content
154
176
 
@@ -171,7 +193,7 @@ class OverleafClient(object):
171
193
  headers = {
172
194
  "X-Csrf-Token": self._csrf
173
195
  }
174
- r = reqs.post(FOLDER_URL.format(project_id),
196
+ r = reqs.post(self._url(FOLDER_PATH.format(project_id)),
175
197
  cookies=self._cookie, headers=headers, json=params)
176
198
 
177
199
  if r.ok:
@@ -219,7 +241,7 @@ class OverleafClient(object):
219
241
  # a session may not carry every cookie -- this previously raised
220
242
  # KeyError here even though overleaf_session2 alone is enough to connect.)
221
243
  handshake = reqs.get(
222
- "{}/socket.io/1/".format(BASE_URL),
244
+ self._url(SOCKET_IO_PATH),
223
245
  params={"t": int(time.time() * 1000)},
224
246
  cookies=self._cookie,
225
247
  )
@@ -237,10 +259,12 @@ class OverleafClient(object):
237
259
  # handshake"); confirmed it rejects even when projectId is only on
238
260
  # the WebSocket upgrade URL -- it must be on the initial
239
261
  # /socket.io/1/ GET specifically.
240
- socket_io = SocketIO(
241
- BASE_URL,
262
+ socket_io = RealtimeSocket(
263
+ self._base_url,
242
264
  params={'t': int(time.time()), 'projectId': project_id},
243
- headers={'Cookie': cookie}
265
+ headers={'Cookie': cookie},
266
+ # Fail instead of retrying forever (and hanging the CLI) when the real-time service is unreachable
267
+ wait_for_connection=False
244
268
  )
245
269
 
246
270
  # Wait until we connect to the socket
@@ -255,15 +279,16 @@ class OverleafClient(object):
255
279
  # .on() never registers -- it would return immediately without
256
280
  # reading anything. Wait in short slices instead, stopping as soon as
257
281
  # the event has actually been processed.
258
- socket_io.on('joinProjectResponse', set_project_infos)
259
- for _ in range(20):
260
- if project_infos is not None:
261
- break
262
- socket_io.wait(seconds=1)
263
-
264
- # Disconnect from the socket if still connected
265
- if socket_io.connected:
266
- socket_io.disconnect()
282
+ try:
283
+ socket_io.on('joinProjectResponse', set_project_infos)
284
+ for _ in range(20):
285
+ if project_infos is not None:
286
+ break
287
+ socket_io.wait(seconds=1)
288
+ finally:
289
+ # Disconnect from the socket if still connected
290
+ if socket_io.connected:
291
+ socket_io.disconnect()
267
292
 
268
293
  return project_infos
269
294
 
@@ -323,7 +348,7 @@ class OverleafClient(object):
323
348
  }
324
349
 
325
350
  # Upload the file to the predefined folder
326
- r = reqs.post(UPLOAD_URL.format(project_id), cookies=self._cookie, params=params, files=files)
351
+ r = reqs.post(self._url(UPLOAD_PATH.format(project_id)), cookies=self._cookie, params=params, files=files)
327
352
 
328
353
  # status_code is an int; comparing to str(200) was always False here,
329
354
  # so this reported failure on every call regardless of the real
@@ -367,7 +392,7 @@ class OverleafClient(object):
367
392
  "X-Csrf-Token": self._csrf
368
393
  }
369
394
 
370
- r = reqs.delete(DELETE_URL.format(project_id, entity_type, file['_id']), cookies=self._cookie,
395
+ r = reqs.delete(self._url(DELETE_PATH.format(project_id, entity_type, file['_id'])), cookies=self._cookie,
371
396
  headers=headers, json={})
372
397
 
373
398
  # status_code is an int; comparing to str(204) was always False
@@ -396,7 +421,7 @@ class OverleafClient(object):
396
421
  "stopOnFirstError": False
397
422
  }
398
423
 
399
- r = reqs.post(COMPILE_URL.format(project_id), cookies=self._cookie, headers=headers, json=body)
424
+ r = reqs.post(self._url(COMPILE_PATH.format(project_id)), cookies=self._cookie, headers=headers, json=body)
400
425
 
401
426
  if not r.ok:
402
427
  raise reqs.HTTPError()
@@ -416,7 +441,7 @@ class OverleafClient(object):
416
441
  if compile_result.get("clsiServerId"):
417
442
  params["clsiserverid"] = compile_result["clsiServerId"]
418
443
 
419
- download_req = reqs.get(BASE_URL + pdf_file['url'], cookies=self._cookie, headers=headers, params=params)
444
+ download_req = reqs.get(self._url(pdf_file['url']), cookies=self._cookie, headers=headers, params=params)
420
445
 
421
446
  if download_req.ok:
422
447
  return pdf_file['path'], download_req.content
@@ -23,10 +23,19 @@ from pathlib import Path
23
23
 
24
24
  try:
25
25
  # Import for pip installation / wheel
26
- from olsync.olclient import OverleafClient
26
+ from olsync.olclient import BASE_URL, OverleafClient
27
27
  except ImportError:
28
28
  # Import for development
29
- from olclient import OverleafClient
29
+ from olclient import BASE_URL, OverleafClient
30
+
31
+ # Session cookie names: overleaf.com, and self-hosted servers (Community Edition, "sharelatex.sid" before version 5)
32
+ OVERLEAF_COM_SESSION_COOKIES = ["overleaf_session2"]
33
+ SELF_HOSTED_SESSION_COOKIES = ["overleaf.sid", "sharelatex.sid"]
34
+
35
+ server_option = click.option(
36
+ '--server', 'server', default=None, envvar='OLSYNC_SERVER', metavar='URL',
37
+ help="URL of a self-hosted Overleaf server (default: the server used at login, or https://www.overleaf.com). "
38
+ "Can also be set with OLSYNC_SERVER.")
30
39
 
31
40
 
32
41
  def configure_console():
@@ -54,20 +63,14 @@ def configure_console():
54
63
  @click.option('-i', '--olignore', 'olignore_path', default=".olignore", type=click.Path(exists=False),
55
64
  help="Path to the .olignore file relative to sync path (ignored if syncing from remote to local). See "
56
65
  "fnmatch / unix filename pattern matching for information on how to use it.")
66
+ @server_option
57
67
  @click.option('-v', '--verbose', 'verbose', is_flag=True, help="Enable extended error logging.")
58
68
  @click.version_option(package_name='overleaf-sync')
59
69
  @click.pass_context
60
- def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path, verbose):
70
+ def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path, server, verbose):
61
71
  configure_console()
62
72
  if ctx.invoked_subcommand is None:
63
- if not os.path.isfile(cookie_path):
64
- raise click.ClickException(
65
- "Persisted Overleaf cookie not found. Please login or check store path.")
66
-
67
- with open(cookie_path, 'rb') as f:
68
- store = pickle.load(f)
69
-
70
- overleaf_client = OverleafClient(store["cookie"], store["csrf"])
73
+ overleaf_client = load_client(cookie_path, server)
71
74
 
72
75
  # Change the current directory to the specified sync path
73
76
  os.chdir(sync_path)
@@ -138,26 +141,31 @@ def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path
138
141
  @click.option('--path', 'cookie_path', default=".olauth", type=click.Path(exists=False),
139
142
  help="Path to store the persisted Overleaf cookie.")
140
143
  @click.option('--cookie', 'use_cookie', is_flag=True,
141
- help="Log in without the login window, using the `overleaf_session2` cookie from a browser that is "
142
- "logged in to Overleaf (asked for at a hidden prompt, or read from OLSYNC_COOKIE). Useful on "
143
- "servers without a display and for single sign-on accounts.")
144
+ help="Log in without the login window, using the session cookie from a browser that is logged in to "
145
+ "Overleaf (`overleaf_session2` on overleaf.com, `overleaf.sid` on self-hosted servers), asked for "
146
+ "at a hidden prompt or read from OLSYNC_COOKIE. Useful on servers without a display and for single "
147
+ "sign-on accounts.")
148
+ @server_option
144
149
  @click.option('-v', '--verbose', 'verbose', is_flag=True, help="Enable extended error logging.")
145
- def login(cookie_path, use_cookie, verbose):
150
+ @click.pass_context
151
+ def login(ctx, cookie_path, use_cookie, server, verbose):
152
+ server = resolve_login_server(ctx, server)
146
153
  if os.path.isfile(cookie_path) and not click.confirm(
147
154
  'Persisted Overleaf cookie already exist. Do you want to override it?'):
148
155
  return
149
156
  if use_cookie:
157
+ cookie_names = OVERLEAF_COM_SESSION_COOKIES if server == BASE_URL else SELF_HOSTED_SESSION_COOKIES
150
158
  # Never accept the cookie as a command line argument, it would end up in the shell history
151
- cookie = os.environ.get("OLSYNC_COOKIE") or click.prompt("Value of the overleaf_session2 cookie",
159
+ cookie = os.environ.get("OLSYNC_COOKIE") or click.prompt(f"Value of the {cookie_names[0]} cookie",
152
160
  hide_input=True)
153
- execute_action(lambda: cookie_login_handler(cookie.strip(), cookie_path), "Login",
161
+ execute_action(lambda: cookie_login_handler(cookie.strip(), cookie_path, server, cookie_names), "Login",
154
162
  "Login successful. Cookie persisted as `" + click.format_filename(
155
163
  cookie_path) + "`. You may now sync your project.",
156
164
  "Login failed: the cookie is not valid (expired, or copied incompletely).", verbose)
157
165
  return
158
166
  olbrowserlogin = load_browser_login()
159
167
  click.clear()
160
- execute_action(lambda: login_handler(olbrowserlogin, cookie_path), "Login",
168
+ execute_action(lambda: login_handler(olbrowserlogin, cookie_path, server), "Login",
161
169
  "Login successful. Cookie persisted as `" + click.format_filename(
162
170
  cookie_path) + "`. You may now sync your project.",
163
171
  "Login failed. Please try again.", verbose)
@@ -166,8 +174,10 @@ def login(cookie_path, use_cookie, verbose):
166
174
  @main.command(name='list')
167
175
  @click.option('--store-path', 'cookie_path', default=".olauth", type=click.Path(exists=False),
168
176
  help="Relative path to load the persisted Overleaf cookie.")
177
+ @server_option
169
178
  @click.option('-v', '--verbose', 'verbose', is_flag=True, help="Enable extended error logging.")
170
- def list_projects(cookie_path, verbose):
179
+ @click.pass_context
180
+ def list_projects(ctx, cookie_path, server, verbose):
171
181
  def query_projects():
172
182
  for index, p in enumerate(sorted(overleaf_client.all_projects(), key=lambda x: x['lastUpdated'], reverse=True)):
173
183
  if not index:
@@ -175,14 +185,7 @@ def list_projects(cookie_path, verbose):
175
185
  click.echo(f"{dateutil.parser.isoparse(p['lastUpdated']).strftime('%m/%d/%Y, %H:%M:%S')} - {p['name']}")
176
186
  return True
177
187
 
178
- if not os.path.isfile(cookie_path):
179
- raise click.ClickException(
180
- "Persisted Overleaf cookie not found. Please login or check store path.")
181
-
182
- with open(cookie_path, 'rb') as f:
183
- store = pickle.load(f)
184
-
185
- overleaf_client = OverleafClient(store["cookie"], store["csrf"])
188
+ overleaf_client = load_client(cookie_path, server or ctx.find_root().params.get("server"))
186
189
 
187
190
  click.clear()
188
191
  execute_action(query_projects, "Querying all projects",
@@ -196,8 +199,10 @@ def list_projects(cookie_path, verbose):
196
199
  @click.option('--download-path', 'download_path', default=".", type=click.Path(exists=True))
197
200
  @click.option('--store-path', 'cookie_path', default=".olauth", type=click.Path(exists=False),
198
201
  help="Relative path to load the persisted Overleaf cookie.")
202
+ @server_option
199
203
  @click.option('-v', '--verbose', 'verbose', is_flag=True, help="Enable extended error logging.")
200
- def download_pdf(project_name, download_path, cookie_path, verbose):
204
+ @click.pass_context
205
+ def download_pdf(ctx, project_name, download_path, cookie_path, server, verbose):
201
206
  def download_project_pdf():
202
207
  nonlocal project_name
203
208
  project_name = project_name or os.path.basename(os.getcwd())
@@ -217,14 +222,7 @@ def download_pdf(project_name, download_path, cookie_path, verbose):
217
222
 
218
223
  return True
219
224
 
220
- if not os.path.isfile(cookie_path):
221
- raise click.ClickException(
222
- "Persisted Overleaf cookie not found. Please login or check store path.")
223
-
224
- with open(cookie_path, 'rb') as f:
225
- store = pickle.load(f)
226
-
227
- overleaf_client = OverleafClient(store["cookie"], store["csrf"])
225
+ overleaf_client = load_client(cookie_path, server or ctx.find_root().params.get("server"))
228
226
 
229
227
  click.clear()
230
228
 
@@ -262,23 +260,46 @@ def load_browser_login():
262
260
  return olbrowserlogin
263
261
 
264
262
 
265
- def login_handler(olbrowserlogin, path):
266
- store = olbrowserlogin.login()
263
+ def resolve_login_server(ctx, server):
264
+ """--server on the login command, else on the group (both also read OLSYNC_SERVER), else overleaf.com"""
265
+ return (server or ctx.find_root().params.get("server") or BASE_URL).rstrip("/")
266
+
267
+
268
+ def load_client(cookie_path, server=None):
269
+ """
270
+ Create an OverleafClient from the persisted login. The server is --server/OLSYNC_SERVER if given, otherwise the
271
+ server stored at login (auth files from before 2.0.0a3 have none and mean overleaf.com).
272
+ """
273
+ if not os.path.isfile(cookie_path):
274
+ raise click.ClickException(
275
+ "Persisted Overleaf cookie not found. Please login or check store path.")
276
+
277
+ with open(cookie_path, 'rb') as f:
278
+ store = pickle.load(f)
279
+
280
+ return OverleafClient(store["cookie"], store["csrf"], base_url=server or store.get("server") or BASE_URL)
281
+
282
+
283
+ def login_handler(olbrowserlogin, path, server):
284
+ store = olbrowserlogin.login(server)
267
285
  if store is None:
268
286
  return False
287
+ store["server"] = server
269
288
  with open(path, 'wb+') as f:
270
289
  pickle.dump(store, f)
271
290
  return True
272
291
 
273
292
 
274
- def cookie_login_handler(cookie, path):
275
- cookies = {"overleaf_session2": cookie}
276
- csrf = OverleafClient(cookies).fetch_csrf()
277
- if csrf is None:
278
- return False
279
- with open(path, 'wb+') as f:
280
- pickle.dump({"cookie": cookies, "csrf": csrf}, f)
281
- return True
293
+ def cookie_login_handler(cookie, path, server, cookie_names):
294
+ # Self-hosted servers name the session cookie differently depending on the version, so try each name
295
+ for cookie_name in cookie_names:
296
+ cookies = {cookie_name: cookie}
297
+ csrf = OverleafClient(cookies, base_url=server).fetch_csrf()
298
+ if csrf is not None:
299
+ with open(path, 'wb+') as f:
300
+ pickle.dump({"cookie": cookies, "csrf": csrf, "server": server}, f)
301
+ return True
302
+ return False
282
303
 
283
304
 
284
305
  def delete_file(path):
File without changes