diff --git a/README.md b/README.md index 3a6b3f0..ce6fdef 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,74 @@ # Nextcloud Deck importieren/exportieren -## Installation +## Voraussetzung -1. Download https://github.com/johappel/nextcloud-import-export/archive/refs/heads/main.zip - oder auf der Komandozeile git clone https://github.com/johappel/nextcloud-import-export.git -2. Gehe in das Verzeichnis nextcloud-import-export (cd nextcloud-import-export) -3. Führe auf der Komandozeile aus:`pip install requests` -4. Kopiere die Datei "sample.config.py" nach "config.py" und trage dort die Daten zu deinen Nextcloudinstanzen ein +[Python](https://www.python.org/downloads/) muss installiert sein. Du kannst diese [Anleitung](https://kinsta.com/de/wissensdatenbank/python-installieren/) nutzen. +## Optional: Git installieren -## Verwendung -Um ein bestimmtes Deck auf eine andfere Nextcloud Instanz zu kopieren gibst du auf der Komandozeile an: +Installiere [Git](https://git-scm.com/book/de/v2/Erste-Schritte-Git-installieren): Das ermöglicht dir, die Skripte aktuell zu halten und dich an der Entwicklung zu beteiligen. -```python -python clone.py --"Name des Decks" -``` -oder -```python -python3 clone.py --"Name des Decks" +## Installation der Skripte + +1. [Download](https://github.com/johappel/nextcloud-import-export/archive/refs/heads/main.zip) der Skripte oder mit Git + `git clone https://github.com/johappel/nextcloud-import-export.git` +2. Gehe in das Verzeichnis nextcloud-import-export (cd nextcloud-import-export) +3. Führe auf der Kommandozeile aus: `pip install requests` +4. Kopiere die Datei "sample.config.py" nach "config.py" und trage dort die Daten zu deinen Nextcloud-Instanzen ein. +5. Mit diesem Befehl auf der Kommandozeile holst du die neueste Version: + `git pull` + +## Anwendungsfälle + +Um ein bestimmtes Deck auf eine andere Nextcloud-Instanz zu kopieren, gibst du auf der Kommandozeile einen der folgenden Befehle ein: + +1. Um ein Deck zu kopieren: + +```sh +python clone.py --board "Name des Decks" ``` -Um alle Decks zu kopieren, gibst du ein: +2. Um ein bestehendes Deck auf der Zielinstanz zu löschen und zu ersetzen: -```python +```sh +python sync.py --board "Name des Decks" --replace +``` + +3. Um ein bestehendes Deck auf der Zielinstanz mit den Daten, Karten und Stacks der Originalinstanz synchron zu halten: + +```sh +python sync.py --board "Name des Decks" +``` + +4. Um ein Backup aller Decks mit Datum des Backups im Titel auf der Zielinstanz zu sichern: + +```sh python backup.py ``` - Dank der großartigen Arbeit von @svbergerem: https://gist.github.com/svbergerem/5914d7f87764901aefddba125af99938 ### Funktionen des Skripts 1. **Daten von der Quellinstanz abrufen:** - - `getBoards()`: Ruft die Liste aller Boards ab. - - `getBoardDetails(boardId)`: Ruft Details eines spezifischen Boards ab. - - `getStacks(boardId)`: Ruft die Stacks eines Boards ab. - - `getStacksArchived(boardId)`: Ruft die archivierten Stacks eines Boards ab. + Gib bei den folgenden Funktionen als Parameter 'from' oder 'to' ein, je nachdem, ob du die Daten von der Quellinstanz oder der Zielinstanz abfragst: + - `getBoards(from_or_to)`: Ruft die Liste aller Boards ab. + - `getBoardDetails(boardId, from_or_to)`: Ruft Details eines spezifischen Boards ab. + - `getStacks(boardId, from_or_to)`: Ruft die Stacks eines Boards ab. + - `getStacksArchived(boardId, from_or_to)`: Ruft die archivierten Stacks eines Boards ab. 2. **Daten zur Zielinstanz übertragen:** - `createBoard(title, color)`: Erstellt ein Board. - `createLabel(title, color, boardId)`: Erstellt ein Label in einem Board. - `createStack(title, order, boardId)`: Erstellt einen Stack in einem Board. - `createCard(title, ctype, order, description, duedate, boardId, stackId)`: Erstellt eine Karte in einem Stack. - - `assignLabel(labelId, cardId, boardId, stackId)`: Weist ein Label einer Karte zu. + - `assignLabel(labelId, cardId, boardId, stackId)`: Weist einer Karte ein Label zu. - `archiveCard(card, boardId, stackId)`: Archiviert eine Karte. - - `copyCard(card, boardIdTo, stackIdTo, labelsMap)`: Kopiert eine Karte, einschließlich ihrer Labels und archiviertem Status. + - `copyCard(card, boardIdTo, stackIdTo, labelsMap)`: Kopiert eine Karte, einschließlich ihrer Labels und des archivierten Status. + - `deleteBoard(boardIdTo)`: Löscht ein Board. + - `deleteStacks(boardIdTo)`: Löscht alle Listen eines Boards. + - `deleteLabels(boardIdTo)`: Löscht alle Karten eines Boards. + +# @todo: + - Synchronisation verbessern (nur dezidierte Karten ersetzen) + - Grafisches User Interface diff --git a/__pycache__/lib.cpython-312.pyc b/__pycache__/lib.cpython-312.pyc index a9ea882..3d4f287 100644 Binary files a/__pycache__/lib.cpython-312.pyc and b/__pycache__/lib.cpython-312.pyc differ diff --git a/backup.py b/backup.py index 634d900..d5448aa 100644 --- a/backup.py +++ b/backup.py @@ -1,4 +1,10 @@ -from .lib import * +from datetime import datetime +from lib import * + +# datetime object containing current date and time +now = datetime.now() +# dd/mm/YY H:M:S +dt_string = now.strftime("_%Y-%m-%d-%H-%M-%S") boards = getBoards() @@ -6,9 +12,9 @@ boards = getBoards() for board in boards: boardIdFrom = board['id'] # create board - createdBoard = createBoard(board['title'], board['color']) + createdBoard = createBoard(board['title']+dt_string, board['color']) boardIdTo = createdBoard['id'] - print('Created board', board['title']) + print('Created board', board['title'], dt_string) # create labels boardDetails = getBoardDetails(board['id']) diff --git a/clone.py b/clone.py index 6bb7feb..b43eb72 100644 --- a/clone.py +++ b/clone.py @@ -4,52 +4,68 @@ import lib # Argumente von der Kommandozeile einlesen parser = argparse.ArgumentParser(description='Klonen eines bestimmten Boards von einer Nextcloud-Instanz zu einer anderen.') parser.add_argument('--board', type=str, required=True, help='Der Titel des zu klonenden Boards.') +parser.add_argument('--replace', action='store_true', help='Ersetze das Board im Ziel, falls es bereits existiert.') args = parser.parse_args() # Board-Titel, den wir klonen möchten board_to_clone = args.board # Hole alle Boards von der Quellinstanz -boards = lib.getBoards() +boards_from = lib.getBoards() # Finde das gewünschte Board -board_to_clone_data = next((board for board in boards if board['title'] == board_to_clone), None) +board_to_clone_data = next((board for board in boards_from if board['title'] == board_to_clone), None) if not board_to_clone_data: print(f'Board "{board_to_clone}" nicht gefunden.') else: boardIdFrom = board_to_clone_data['id'] - # Erstelle das Board in der Zielinstanz - createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color']) - boardIdTo = createdBoard['id'] - print(f'Board "{board_to_clone}" erstellt') + + # Überprüfe, ob das Board im Ziel bereits existiert + boards_to = lib.getBoards('to') + existing_board_to = next((board for board in boards_to if board['title'] == board_to_clone and 0 == board['deletedAt']), None) + + # Löschen wenn der parameter --replace gesetzt wurde und das Board existiert + if args.replace and existing_board_to: + # Lösche das bestehende Board im Ziel + print(f'Lösche Board: {existing_board_to["id"]}') + lib.deleteBoard(existing_board_to['id']) + print(f'Board "{board_to_clone}" im Ziel gelöscht') - # Kopiere die Labels des Boards - boardDetails = lib.getBoardDetails(boardIdFrom) - labelsMap = {} - for label in boardDetails['labels']: - createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) - labelsMap[label['id']] = createdLabel['id'] + # Erstelle das Board in der Zielinstanz, wenn es nicht existiert oder ersetzt werden soll + if not existing_board_to or args.replace: + createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color']) + boardIdTo = createdBoard['id'] + print(f'Board "{board_to_clone}" erstellt') - # Kopiere die Stacks und Karten des Boards - stacks = lib.getStacks(boardIdFrom) - stacksMap = {} - for stack in stacks: - createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) - stackIdTo = createdStack['id'] - stacksMap[stack['id']] = stackIdTo - print(f' Stapel "{stack['title']}" erstellt') + # Kopiere die Labels des Boards + boardDetails = lib.getBoardDetails(boardIdFrom) + labelsMap = {} + for label in boardDetails['labels']: + createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) + labelsMap[label['id']] = createdLabel['id'] - if 'cards' in stack: - for card in stack['cards']: - lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) - print(f' {len(stack["cards"])} Karten erstellt') + # Kopiere die Stacks und Karten des Boards + stacks = lib.getStacks(boardIdFrom) + stacksMap = {} + for stack in stacks: + createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) + stackIdTo = createdStack['id'] + stacksMap[stack['id']] = stackIdTo + print(f' Stapel "{stack["title"]}" erstellt') - # Kopiere die archivierten Stacks und Karten des Boards - stacks = lib.getStacksArchived(boardIdFrom) - for stack in stacks: - if 'cards' in stack: - print(f' Stack "{stack['title']}"') - for card in stack['cards']: - lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap) - print(f' {len(stack["cards"])} archivierte Karten erstellt') + if 'cards' in stack: + for card in stack['cards']: + lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) + print(f' {len(stack["cards"])} Karten erstellt') + + # Kopiere die archivierten Stacks und Karten des Boards + stacks = lib.getStacksArchived(boardIdFrom) + for stack in stacks: + if 'cards' in stack: + print(f' Stack "{stack["title"]}"') + for card in stack['cards']: + lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap) + print(f' {len(stack["cards"])} archivierte Karten erstellt') + else: + print(f'Board "{board_to_clone}" existiert bereits und wird nicht ersetzt (verwenden Sie --replace, um zu ersetzen).') diff --git a/lib.py b/lib.py index 41027d7..68f654c 100644 --- a/lib.py +++ b/lib.py @@ -1,9 +1,6 @@ -# thanks to the awesome work of @svbergerem -# -> svbergerem/nextcloud-deck-export-import.py -# https://gist.github.com/svbergerem/5914d7f87764901aefddba125af99938 - import requests import config +import base64 urlFrom = config.urlFrom authFrom = config.authFrom @@ -11,123 +8,66 @@ authFrom = config.authFrom urlTo = config.urlTo authTo = config.authTo - headers = {'OCS-APIRequest': 'true', 'Content-Type': 'application/json'} -def getBoards(): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards', - auth=authFrom, - headers=headers) +def make_request(method, endpoint, from_to='from', json=None): + if from_to == 'from': + url = urlFrom + auth = authFrom + else: # from_to == 'to' + url = urlTo + auth = authTo + + response = requests.request(method, f'{url}{endpoint}', auth=auth, headers=headers, json=json) response.raise_for_status() return response.json() -def getBoardDetails(boardId): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}', - auth=authFrom, - headers=headers) - response.raise_for_status() - return response.json() +def getBoards(from_to='from'): + boards = make_request('GET', '/index.php/apps/deck/api/v1.0/boards', from_to) + return [board for board in boards if 0 == board['deletedAt']] -def getStacks(boardId): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', - auth=authFrom, - headers=headers) - response.raise_for_status() - return response.json() +def getBoardDetails(boardId, from_to='from'): + return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}', from_to) -def getStacksArchived(boardId): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/archived', - auth=authFrom, - headers=headers) - response.raise_for_status() - return response.json() +def getStacks(boardId, from_to='from'): + return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', from_to) + +def getStacksArchived(boardId, from_to='from'): + return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/archived', from_to) def createBoard(title, color): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards', - auth=authTo, - json={ - 'title': title, - 'color': color - }, - headers=headers) - response.raise_for_status() - board = response.json() + board = make_request('POST', '/index.php/apps/deck/api/v1.0/boards', 'to', json={'title': title, 'color': color}) boardId = board['id'] # remove all default labels for label in board['labels']: labelId = label['id'] - response = requests.delete( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{labelId}', - auth=authTo, - headers=headers) - response.raise_for_status() + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{labelId}', 'to') return board def createLabel(title, color, boardId): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/labels', - auth=authTo, - json={ - 'title': title, - 'color': color - }, - headers=headers) - response.raise_for_status() - return response.json() + return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels', 'to', json={'title': title, 'color': color}) def createStack(title, order, boardId): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', - auth=authTo, - json={ - 'title': title, - 'order': order - }, - headers=headers) - response.raise_for_status() - return response.json() + return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', 'to', json={'title': title, 'order': order}) def createCard(title, ctype, order, description, duedate, boardId, stackId): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', - auth=authTo, - json={ - 'title': title, - 'type': ctype, - 'order': order, - 'description': description, - 'duedate': duedate - }, - headers=headers) - response.raise_for_status() - return response.json() + try: + return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', 'to', + json={'title': title, 'type': ctype, 'order': order, 'description': description, 'duedate': duedate}) + except requests.exceptions.HTTPError as e: + print(f"Error creating card: {e}") + print(f"Response: {e.response.text}") + raise def assignLabel(labelId, cardId, boardId, stackId): - response = requests.put( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', - auth=authTo, - json={ - 'labelId': labelId - }, - headers=headers) - response.raise_for_status() + make_request('PUT', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', 'to', json={'labelId': labelId}) def archiveCard(card, boardId, stackId): - cardId = card['id'] card['archived'] = True - response = requests.put( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}', - auth=authTo, - json=card, - headers=headers) - response.raise_for_status() + make_request('PUT', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{card["id"]}', 'to', json=card) def copyCard(card, boardIdTo, stackIdTo, labelsMap): + print(f"Copying card '{card['title']}' to board {boardIdTo}, stack {stackIdTo}") createdCard = createCard( card['title'], card['type'], @@ -146,4 +86,16 @@ def copyCard(card, boardIdTo, stackIdTo, labelsMap): if card['archived']: archiveCard(createdCard, boardIdTo, stackIdTo) +# Löschfunktionen auf der Zielinstanz +def deleteBoard(boardId): + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}', 'to') +def deleteStacks(boardId): + stacks = getStacks(boardId, 'to') + for stack in stacks: + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stack["id"]}', 'to') + +def deleteLabels(boardId): + boardDetails = getBoardDetails(boardId, 'to') + for label in boardDetails['labels']: + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{label["id"]}', 'to') diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..35d9cdd --- /dev/null +++ b/sync.py @@ -0,0 +1,107 @@ +import argparse +import requests +import lib + +# Argumente von der Kommandozeile einlesen +parser = argparse.ArgumentParser(description='Clone or sync a specific board from one Nextcloud instance to another.') +parser.add_argument('--board', type=str, required=True, help='The title of the board to clone or sync.') +args = parser.parse_args() + +# Board-Titel, den wir klonen oder synchronisieren möchten +board_to_clone = args.board + +# Hole alle Boards von der Quellinstanz +source_boards = lib.getBoards() + +# Finde das gewünschte Board in der Quellinstanz +board_to_clone_data = next((board for board in source_boards if board['title'] == board_to_clone), None) + +if not board_to_clone_data: + print(f'Board "{board_to_clone}" nicht gefunden.') + exit() + +boardIdFrom = board_to_clone_data['id'] + +# Hole alle Boards von der Zielinstanz +target_boards = lib.getBoards('to') + +# Überprüfen, ob das Board in der Zielinstanz existiert +target_board_data = next((board for board in target_boards if board['title'] == board_to_clone), None) + +if target_board_data: + boardIdTo = target_board_data['id'] + print(f'Board "{board_to_clone}" already exists. Syncing...') +else: + # Erstelle das Board in der Zielinstanz + createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color']) + boardIdTo = createdBoard['id'] + print(f'Created board "{board_to_clone}"') + +# Kopiere oder synchronisiere die Labels des Boards +boardDetails = lib.getBoardDetails(boardIdFrom) +labelsMap = {} +target_board_details = lib.getBoardDetails(boardIdTo,'to') + +# Existierende Labels in der Zielinstanz sammeln +existing_labels = {label['title']: label['id'] for label in target_board_details['labels']} + +for label in boardDetails['labels']: + if label['title'] in existing_labels: + labelsMap[label['id']] = existing_labels[label['title']] + else: + createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) + labelsMap[label['id']] = createdLabel['id'] + +# Kopiere oder synchronisiere die Stacks und Karten des Boards +stacks = lib.getStacks(boardIdFrom) +target_stacks = lib.getStacks(boardIdTo,'to') +stacksMap = {} + +# Existierende Stacks in der Zielinstanz sammeln +existing_stacks = {stack['title']: stack['id'] for stack in target_stacks} + +for stack in stacks: + if stack['title'] in existing_stacks: + stackIdTo = existing_stacks[stack['title']] + stacksMap[stack['id']] = stackIdTo + print(f' Stack "{stack["title"]}" already exists. Syncing...') + else: + createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) + stackIdTo = createdStack['id'] + stacksMap[stack['id']] = stackIdTo + print(f' Created stack "{stack["title"]}"') + + if 'cards' in stack: + for card in stack['cards']: + try: + lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) + except requests.exceptions.HTTPError as e: + print(f' Failed to create card "{card["title"]}". Error: {e}') + print(f' Response: {e.response.text}') + print(f' Created {len(stack["cards"])} cards') + +# Kopiere oder synchronisiere die archivierten Stacks und Karten des Boards +archived_stacks = lib.getStacksArchived(boardIdFrom) +target_archived_stacks = lib.getStacksArchived(boardIdTo,'to') + +# Existierende archivierte Stacks in der Zielinstanz sammeln +existing_archived_stacks = {stack['title']: stack['id'] for stack in target_archived_stacks} + +for stack in archived_stacks: + if stack['title'] in existing_archived_stacks: + stackIdTo = existing_archived_stacks[stack['title']] + print(f' Archived stack "{stack['title']}" already exists. Syncing...') + else: + createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) + stackIdTo = createdStack['id'] + stacksMap[stack['id']] = stackIdTo + print(f' Created archived stack "{stack['title']}"') + + if 'cards' in stack: + for card in stack['cards']: + try: + lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) + except requests.exceptions.HTTPError as e: + print(f' Failed to create archived card "{card["title"]}". Error: {e}') + print(f' Response: {e.response.text}') + print(f' Created {len(stack["cards"])} archived cards')