diff --git a/olsync/olclient.py b/olsync/olclient.py index a993de1..b7a7866 100644 --- a/olsync/olclient.py +++ b/olsync/olclient.py @@ -77,9 +77,17 @@ def login(self, username, password): # CSRF changes after making the login request, new CSRF token will be on the projects page projects_page = reqs.get(PROJECT_URL, cookies=self._cookie) - self._csrf = BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-csrfToken'}) \ - .get('content') - + soup = BeautifulSoup(projects_page.content, 'html.parser') + + # Try new Overleaf structure first + csrf_meta = soup.find('meta', {'name': 'ol-csrfToken'}) + if not csrf_meta: + # Fallback to old method + csrf_meta = soup.find('input', {'name': '_csrf'}) + if not csrf_meta: + raise Exception("Unable to find CSRF token - login may have failed") + + self._csrf = csrf_meta.get('content') or csrf_meta.get('value') return {"cookie": self._cookie, "csrf": self._csrf} def all_projects(self): @@ -88,8 +96,25 @@ def all_projects(self): Returns: List of project objects """ projects_page = reqs.get(PROJECT_URL, cookies=self._cookie) - json_content = json.loads( - BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-projects'}).get('content')) + + if not projects_page.ok: + raise Exception(f"Failed to fetch projects: HTTP {projects_page.status_code}") + + soup = BeautifulSoup(projects_page.content, 'html.parser') + + # Try to find projects data in the prefetched projects meta tag (new Overleaf structure) + projects_meta = soup.find('meta', {'name': 'ol-prefetchedProjectsBlob'}) + if projects_meta: + data = json.loads(projects_meta.get('content')) + if 'projects' in data: + return list(OverleafClient.filter_projects(data['projects'])) + + # Fallback to old method + projects_meta = soup.find('meta', {'name': 'ol-projects'}) + if not projects_meta: + raise Exception("Unable to find projects data - your session may have expired. Please try logging in again.") + + json_content = json.loads(projects_meta.get('content')) return list(OverleafClient.filter_projects(json_content)) def get_project(self, project_name): @@ -98,10 +123,22 @@ def get_project(self, project_name): Params: project_name, the name of the project Returns: project object """ - projects_page = reqs.get(PROJECT_URL, cookies=self._cookie) - json_content = json.loads( - BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-projects'}).get('content')) + soup = BeautifulSoup(projects_page.content, 'html.parser') + + # Try new Overleaf structure first + projects_meta = soup.find('meta', {'name': 'ol-prefetchedProjectsBlob'}) + if projects_meta: + data = json.loads(projects_meta.get('content')) + if 'projects' in data: + return next(OverleafClient.filter_projects(data['projects'], {"name": project_name}), None) + + # Fallback to old method + projects_meta = soup.find('meta', {'name': 'ol-projects'}) + if not projects_meta: + raise Exception("Unable to find projects data - your session may have expired. Please try logging in again.") + + json_content = json.loads(projects_meta.get('content')) return next(OverleafClient.filter_projects(json_content, {"name": project_name}), None) def download_project(self, project_id): @@ -153,41 +190,61 @@ def get_project_infos(self, project_id): Returns: project details """ - project_infos = None - - # Callback function for the joinProject emitter - def set_project_infos(a, project_infos_dict, c, d): - # Set project_infos variable in outer scope - nonlocal project_infos - project_infos = project_infos_dict - - # Convert cookie from CookieJar to string - cookie = "GCLB={}; overleaf_session2={}" \ - .format( - self._cookie["GCLB"], - self._cookie["overleaf_session2"] - ) - - # Connect to Overleaf Socket.IO, send a time parameter and the cookies - socket_io = SocketIO( - BASE_URL, - params={'t': int(time.time())}, - headers={'Cookie': cookie} - ) - - # Wait until we connect to the socket - socket_io.on('connect', lambda: None) - socket_io.wait_for_callbacks() - - # Send the joinProject event and receive the project infos - socket_io.emit('joinProject', {'project_id': project_id}, set_project_infos) - socket_io.wait_for_callbacks() - - # Disconnect from the socket if still connected - if socket_io.connected: - socket_io.disconnect() - - return project_infos + headers = { + "X-Csrf-Token": self._csrf + } + + r = reqs.get(f"{PROJECT_URL}/{project_id}", cookies=self._cookie, headers=headers) + + if not r.ok: + raise Exception(f"Failed to fetch project: HTTP {r.status_code}") + + soup = BeautifulSoup(r.content, 'html.parser') + + # Try to get project data from ExposedSettings + settings_meta = soup.find('meta', {'name': 'ol-ExposedSettings'}) + if settings_meta: + try: + data = json.loads(settings_meta.get('content')) + if 'project' in data: + return data['project'] + print(f"ol-ExposedSettings found but no project key. Keys: {list(data.keys())}") + except json.JSONDecodeError as e: + print(f"Failed to parse ol-ExposedSettings: {e}") + + # Try other meta tags if needed + for meta_name in ['ol-data', 'ol-project']: + project_meta = soup.find('meta', {'name': meta_name}) + if project_meta: + try: + data = json.loads(project_meta.get('content')) + if meta_name == 'ol-data' and 'project' in data: + return data['project'] + elif meta_name == 'ol-project': + return data + except json.JSONDecodeError as e: + print(f"Failed to parse {meta_name}: {e}") + + # If we get here, let's try to construct project info from individual meta tags + project_id_meta = soup.find('meta', {'name': 'ol-project_id'}) + project_name_meta = soup.find('meta', {'name': 'ol-projectName'}) + + if project_id_meta and project_name_meta: + try: + return { + "_id": project_id_meta.get('content'), + "name": project_name_meta.get('content') + } + except Exception as e: + print(f"Failed to construct project info from meta tags: {e}") + + # Debug output + all_meta = soup.find_all('meta') + meta_names = [meta.get('name') for meta in all_meta if meta.get('name')] + print(f"Found meta tags: {meta_names}") + print(f"Response content preview: {str(r.content)[:200]}...") + + raise Exception("Unable to find project data - your session may have expired. Please try logging in again.") def upload_file(self, project_id, project_infos, file_name, file_size, file): """ @@ -201,14 +258,27 @@ def upload_file(self, project_id, project_infos, file_name, file_size, file): Returns: True on success, False on fail """ - - # Set the folder_id to the id of the root folder - folder_id = project_infos['rootFolder'][0]['_id'] + # First get the root folder ID via API if not in project_infos + if 'rootFolder' not in project_infos: + # Use project_id as root folder id - this is often the case in Overleaf + folder_id = project_id + folder_data = {"_id": folder_id, "folders": []} + + print(f"Using project ID as root folder ID: {folder_id}") + else: + # Use existing structure if available + folder_id = project_infos['rootFolder'][0]['_id'] + folder_data = project_infos['rootFolder'][0] # The file name contains path separators, check folders if PATH_SEP in file_name: local_folders = file_name.split(PATH_SEP)[:-1] # Remove last item since this is the file name - current_overleaf_folder = project_infos['rootFolder'][0]['folders'] # Set the current remote folder + + # Get current folder structure if needed + if 'rootFolder' not in project_infos: + current_overleaf_folder = folder_data.get('folders', []) + else: + current_overleaf_folder = project_infos['rootFolder'][0]['folders'] for local_folder in local_folders: exists_on_remote = False @@ -222,9 +292,13 @@ def upload_file(self, project_id, project_infos, file_name, file_size, file): # Create the folder if it doesn't exist if not exists_on_remote: new_folder = self.create_folder(project_id, folder_id, local_folder) - current_overleaf_folder.append(new_folder) - folder_id = new_folder['_id'] - current_overleaf_folder = new_folder['folders'] + if new_folder: # Check if folder creation was successful + current_overleaf_folder.append(new_folder) + folder_id = new_folder['_id'] + current_overleaf_folder = new_folder['folders'] + else: + raise Exception(f"Failed to create folder: {local_folder}") + params = { "folder_id": folder_id, "_csrf": self._csrf, @@ -239,7 +313,18 @@ def upload_file(self, project_id, project_infos, file_name, file_size, file): # Upload the file to the predefined folder r = reqs.post(UPLOAD_URL.format(project_id), cookies=self._cookie, params=params, files=files) - return r.status_code == str(200) and json.loads(r.content)["success"] + if not r.ok: + print(f"Upload failed with status {r.status_code}") + print(f"Response content: {r.content[:200]}...") + return False + + try: + response_data = r.json() + return response_data.get("success", False) + except json.JSONDecodeError as e: + print(f"Failed to parse response: {str(e)}") + print(f"Response content: {r.content[:200]}...") + return False def delete_file(self, project_id, project_infos, file_name): """