- recoded lib

- add sync.py (synchronisize boards)
This commit is contained in:
Joachim Happel 2024-05-28 15:22:28 +02:00
parent 7bd62d9ec9
commit 271f9b3469
6 changed files with 260 additions and 152 deletions

View file

@ -1,47 +1,74 @@
# Nextcloud Deck importieren/exportieren # Nextcloud Deck importieren/exportieren
## Installation ## Voraussetzung
1. Download https://github.com/johappel/nextcloud-import-export/archive/refs/heads/main.zip [Python](https://www.python.org/downloads/) muss installiert sein. Du kannst diese [Anleitung](https://kinsta.com/de/wissensdatenbank/python-installieren/) nutzen.
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
## Optional: Git installieren
## Verwendung 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.
Um ein bestimmtes Deck auf eine andfere Nextcloud Instanz zu kopieren gibst du auf der Komandozeile an:
```python ## Installation der Skripte
python clone.py --"Name des Decks"
``` 1. [Download](https://github.com/johappel/nextcloud-import-export/archive/refs/heads/main.zip) der Skripte oder mit Git
oder `git clone https://github.com/johappel/nextcloud-import-export.git`
```python 2. Gehe in das Verzeichnis nextcloud-import-export (cd nextcloud-import-export)
python3 clone.py --"Name des Decks" 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 python backup.py
``` ```
Dank der großartigen Arbeit von @svbergerem: Dank der großartigen Arbeit von @svbergerem:
https://gist.github.com/svbergerem/5914d7f87764901aefddba125af99938 https://gist.github.com/svbergerem/5914d7f87764901aefddba125af99938
### Funktionen des Skripts ### Funktionen des Skripts
1. **Daten von der Quellinstanz abrufen:** 1. **Daten von der Quellinstanz abrufen:**
- `getBoards()`: Ruft die Liste aller 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:
- `getBoardDetails(boardId)`: Ruft Details eines spezifischen Boards ab. - `getBoards(from_or_to)`: Ruft die Liste aller Boards ab.
- `getStacks(boardId)`: Ruft die Stacks eines Boards ab. - `getBoardDetails(boardId, from_or_to)`: Ruft Details eines spezifischen Boards ab.
- `getStacksArchived(boardId)`: Ruft die archivierten Stacks eines 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:** 2. **Daten zur Zielinstanz übertragen:**
- `createBoard(title, color)`: Erstellt ein Board. - `createBoard(title, color)`: Erstellt ein Board.
- `createLabel(title, color, boardId)`: Erstellt ein Label in einem Board. - `createLabel(title, color, boardId)`: Erstellt ein Label in einem Board.
- `createStack(title, order, boardId)`: Erstellt einen Stack 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. - `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. - `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

Binary file not shown.

View file

@ -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() boards = getBoards()
@ -6,9 +12,9 @@ boards = getBoards()
for board in boards: for board in boards:
boardIdFrom = board['id'] boardIdFrom = board['id']
# create board # create board
createdBoard = createBoard(board['title'], board['color']) createdBoard = createBoard(board['title']+dt_string, board['color'])
boardIdTo = createdBoard['id'] boardIdTo = createdBoard['id']
print('Created board', board['title']) print('Created board', board['title'], dt_string)
# create labels # create labels
boardDetails = getBoardDetails(board['id']) boardDetails = getBoardDetails(board['id'])

View file

@ -4,52 +4,68 @@ import lib
# Argumente von der Kommandozeile einlesen # Argumente von der Kommandozeile einlesen
parser = argparse.ArgumentParser(description='Klonen eines bestimmten Boards von einer Nextcloud-Instanz zu einer anderen.') 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('--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() args = parser.parse_args()
# Board-Titel, den wir klonen möchten # Board-Titel, den wir klonen möchten
board_to_clone = args.board board_to_clone = args.board
# Hole alle Boards von der Quellinstanz # Hole alle Boards von der Quellinstanz
boards = lib.getBoards() boards_from = lib.getBoards()
# Finde das gewünschte Board # 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: if not board_to_clone_data:
print(f'Board "{board_to_clone}" nicht gefunden.') print(f'Board "{board_to_clone}" nicht gefunden.')
else: else:
boardIdFrom = board_to_clone_data['id'] 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']) # Überprüfe, ob das Board im Ziel bereits existiert
boardIdTo = createdBoard['id'] boards_to = lib.getBoards('to')
print(f'Board "{board_to_clone}" erstellt') 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 # Erstelle das Board in der Zielinstanz, wenn es nicht existiert oder ersetzt werden soll
boardDetails = lib.getBoardDetails(boardIdFrom) if not existing_board_to or args.replace:
labelsMap = {} createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color'])
for label in boardDetails['labels']: boardIdTo = createdBoard['id']
createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) print(f'Board "{board_to_clone}" erstellt')
labelsMap[label['id']] = createdLabel['id']
# Kopiere die Stacks und Karten des Boards # Kopiere die Labels des Boards
stacks = lib.getStacks(boardIdFrom) boardDetails = lib.getBoardDetails(boardIdFrom)
stacksMap = {} labelsMap = {}
for stack in stacks: for label in boardDetails['labels']:
createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo)
stackIdTo = createdStack['id'] labelsMap[label['id']] = createdLabel['id']
stacksMap[stack['id']] = stackIdTo
print(f' Stapel "{stack['title']}" erstellt')
if 'cards' in stack: # Kopiere die Stacks und Karten des Boards
for card in stack['cards']: stacks = lib.getStacks(boardIdFrom)
lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) stacksMap = {}
print(f' {len(stack["cards"])} Karten erstellt') 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 if 'cards' in stack:
stacks = lib.getStacksArchived(boardIdFrom) for card in stack['cards']:
for stack in stacks: lib.copyCard(card, boardIdTo, stackIdTo, labelsMap)
if 'cards' in stack: print(f' {len(stack["cards"])} Karten erstellt')
print(f' Stack "{stack['title']}"')
for card in stack['cards']: # Kopiere die archivierten Stacks und Karten des Boards
lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap) stacks = lib.getStacksArchived(boardIdFrom)
print(f' {len(stack["cards"])} archivierte Karten erstellt') 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).')

140
lib.py
View file

@ -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 requests
import config import config
import base64
urlFrom = config.urlFrom urlFrom = config.urlFrom
authFrom = config.authFrom authFrom = config.authFrom
@ -11,123 +8,66 @@ authFrom = config.authFrom
urlTo = config.urlTo urlTo = config.urlTo
authTo = config.authTo authTo = config.authTo
headers = {'OCS-APIRequest': 'true', 'Content-Type': 'application/json'} headers = {'OCS-APIRequest': 'true', 'Content-Type': 'application/json'}
def getBoards(): def make_request(method, endpoint, from_to='from', json=None):
response = requests.get( if from_to == 'from':
f'{urlFrom}/index.php/apps/deck/api/v1.0/boards', url = urlFrom
auth=authFrom, auth = authFrom
headers=headers) 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() response.raise_for_status()
return response.json() return response.json()
def getBoardDetails(boardId): def getBoards(from_to='from'):
response = requests.get( boards = make_request('GET', '/index.php/apps/deck/api/v1.0/boards', from_to)
f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}', return [board for board in boards if 0 == board['deletedAt']]
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getStacks(boardId): def getBoardDetails(boardId, from_to='from'):
response = requests.get( return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}', from_to)
f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getStacksArchived(boardId): def getStacks(boardId, from_to='from'):
response = requests.get( return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', from_to)
f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/archived',
auth=authFrom, def getStacksArchived(boardId, from_to='from'):
headers=headers) return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/archived', from_to)
response.raise_for_status()
return response.json()
def createBoard(title, color): def createBoard(title, color):
response = requests.post( board = make_request('POST', '/index.php/apps/deck/api/v1.0/boards', 'to', json={'title': title, 'color': color})
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()
boardId = board['id'] boardId = board['id']
# remove all default labels # remove all default labels
for label in board['labels']: for label in board['labels']:
labelId = label['id'] labelId = label['id']
response = requests.delete( make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{labelId}', 'to')
f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{labelId}',
auth=authTo,
headers=headers)
response.raise_for_status()
return board return board
def createLabel(title, color, boardId): def createLabel(title, color, boardId):
response = requests.post( return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels', 'to', json={'title': title, 'color': color})
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()
def createStack(title, order, boardId): def createStack(title, order, boardId):
response = requests.post( return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', 'to', json={'title': title, 'order': order})
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()
def createCard(title, ctype, order, description, duedate, boardId, stackId): def createCard(title, ctype, order, description, duedate, boardId, stackId):
response = requests.post( try:
f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', 'to',
auth=authTo, json={'title': title, 'type': ctype, 'order': order, 'description': description, 'duedate': duedate})
json={ except requests.exceptions.HTTPError as e:
'title': title, print(f"Error creating card: {e}")
'type': ctype, print(f"Response: {e.response.text}")
'order': order, raise
'description': description,
'duedate': duedate
},
headers=headers)
response.raise_for_status()
return response.json()
def assignLabel(labelId, cardId, boardId, stackId): def assignLabel(labelId, cardId, boardId, stackId):
response = requests.put( make_request('PUT', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', 'to', json={'labelId': labelId})
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()
def archiveCard(card, boardId, stackId): def archiveCard(card, boardId, stackId):
cardId = card['id']
card['archived'] = True card['archived'] = True
response = requests.put( make_request('PUT', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{card["id"]}', 'to', json=card)
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()
def copyCard(card, boardIdTo, stackIdTo, labelsMap): def copyCard(card, boardIdTo, stackIdTo, labelsMap):
print(f"Copying card '{card['title']}' to board {boardIdTo}, stack {stackIdTo}")
createdCard = createCard( createdCard = createCard(
card['title'], card['title'],
card['type'], card['type'],
@ -146,4 +86,16 @@ def copyCard(card, boardIdTo, stackIdTo, labelsMap):
if card['archived']: if card['archived']:
archiveCard(createdCard, boardIdTo, stackIdTo) 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')

107
sync.py Normal file
View file

@ -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')