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.
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/PKG-INFO +11 -2
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/README.md +10 -1
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/olsync/__init__.py +1 -1
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/olsync/olbrowserlogin.py +13 -10
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/olsync/olclient.py +61 -36
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/olsync/olsync.py +67 -46
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/LICENSE +0 -0
- {overleaf_sync-2.0.0a2 → overleaf_sync-2.0.0a3}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: overleaf-sync
|
|
3
|
-
Version: 2.0.
|
|
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]
|
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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(
|
|
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() ==
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# The
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
241
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
socket_io.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
142
|
-
"
|
|
143
|
-
"servers without a display and for single
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
266
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
File without changes
|