- 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
## Optional: Git installieren
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.
## 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) 2. Gehe in das Verzeichnis nextcloud-import-export (cd nextcloud-import-export)
3. Führe auf der Komandozeile aus:`pip install requests` 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 Nextcloudinstanzen ein 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
## Verwendung Um ein bestimmtes Deck auf eine andere Nextcloud-Instanz zu kopieren, gibst du auf der Kommandozeile einen der folgenden Befehle ein:
Um ein bestimmtes Deck auf eine andfere Nextcloud Instanz zu kopieren gibst du auf der Komandozeile an:
```python 1. Um ein Deck zu kopieren:
python clone.py --"Name des Decks"
``` ```sh
oder python clone.py --board "Name des Decks"
```python
python3 clone.py --"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,22 +4,36 @@ 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
# Ü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')
# 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']) createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color'])
boardIdTo = createdBoard['id'] boardIdTo = createdBoard['id']
print(f'Board "{board_to_clone}" erstellt') print(f'Board "{board_to_clone}" erstellt')
@ -38,7 +52,7 @@ else:
createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo)
stackIdTo = createdStack['id'] stackIdTo = createdStack['id']
stacksMap[stack['id']] = stackIdTo stacksMap[stack['id']] = stackIdTo
print(f' Stapel "{stack['title']}" erstellt') print(f' Stapel "{stack["title"]}" erstellt')
if 'cards' in stack: if 'cards' in stack:
for card in stack['cards']: for card in stack['cards']:
@ -49,7 +63,9 @@ else:
stacks = lib.getStacksArchived(boardIdFrom) stacks = lib.getStacksArchived(boardIdFrom)
for stack in stacks: for stack in stacks:
if 'cards' in stack: if 'cards' in stack:
print(f' Stack "{stack['title']}"') print(f' Stack "{stack["title"]}"')
for card in stack['cards']: for card in stack['cards']:
lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap) lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap)
print(f' {len(stack["cards"])} archivierte Karten erstellt') 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')