znote 1.0.1__tar.gz → 1.2.3__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.
Files changed (30) hide show
  1. {znote-1.0.1/znote.egg-info → znote-1.2.3}/PKG-INFO +44 -1
  2. {znote-1.0.1 → znote-1.2.3}/README.md +43 -0
  3. {znote-1.0.1 → znote-1.2.3}/znote/app/display.py +72 -1
  4. {znote-1.0.1 → znote-1.2.3}/znote/client.py +210 -2
  5. znote-1.2.3/znote/core/client/fetch.py +83 -0
  6. znote-1.2.3/znote/core/client/metafile.py +12 -0
  7. {znote-1.0.1 → znote-1.2.3}/znote/core/client/settings.py +14 -6
  8. {znote-1.0.1 → znote-1.2.3}/znote/core/server/settings.py +13 -8
  9. {znote-1.0.1 → znote-1.2.3}/znote/model/models.py +17 -1
  10. {znote-1.0.1 → znote-1.2.3}/znote/server.py +164 -1
  11. {znote-1.0.1 → znote-1.2.3/znote.egg-info}/PKG-INFO +44 -1
  12. {znote-1.0.1 → znote-1.2.3}/znote.egg-info/SOURCES.txt +2 -0
  13. {znote-1.0.1 → znote-1.2.3}/LICENSE +0 -0
  14. {znote-1.0.1 → znote-1.2.3}/setup.cfg +0 -0
  15. {znote-1.0.1 → znote-1.2.3}/znote/__init__.py +0 -0
  16. {znote-1.0.1 → znote-1.2.3}/znote/app/input.py +0 -0
  17. {znote-1.0.1 → znote-1.2.3}/znote/core/auth.py +0 -0
  18. {znote-1.0.1 → znote-1.2.3}/znote/core/client/connect.py +0 -0
  19. {znote-1.0.1 → znote-1.2.3}/znote/core/client/token.py +0 -0
  20. {znote-1.0.1 → znote-1.2.3}/znote/core/config.py +0 -0
  21. {znote-1.0.1 → znote-1.2.3}/znote/core/editor.py +0 -0
  22. {znote-1.0.1 → znote-1.2.3}/znote/core/gen.py +0 -0
  23. {znote-1.0.1 → znote-1.2.3}/znote/core/pack.py +0 -0
  24. {znote-1.0.1 → znote-1.2.3}/znote/core/server/note.py +0 -0
  25. {znote-1.0.1 → znote-1.2.3}/znote/core/workspace.py +0 -0
  26. {znote-1.0.1 → znote-1.2.3}/znote/main.py +0 -0
  27. {znote-1.0.1 → znote-1.2.3}/znote.egg-info/dependency_links.txt +0 -0
  28. {znote-1.0.1 → znote-1.2.3}/znote.egg-info/entry_points.txt +0 -0
  29. {znote-1.0.1 → znote-1.2.3}/znote.egg-info/requires.txt +0 -0
  30. {znote-1.0.1 → znote-1.2.3}/znote.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: znote
3
- Version: 1.0.1
3
+ Version: 1.2.3
4
4
  Summary: Vault de prise de note
5
5
  Author:
6
6
  License: mit
@@ -40,6 +40,7 @@ znote est une application de prise de notes en ligne de commande fonctionnant su
40
40
  - **Éditeur Intégré** : Un éditeur de texte simple est intégré directement dans le terminal pour une expérience fluide.
41
41
  - **Workspaces (Espaces de travail)** : Organisez vos notes dans des espaces de travail distincts.
42
42
  - **Synchronisation de Fichiers** : Envoyez (`push`) un fichier local pour créer une note, ou téléchargez (`pull`) une note dans un fichier.
43
+ - **Channels de Diffusion** : Partagez et synchronisez des fichiers sur plusieurs postes via des "channels".
43
44
  - **Protection par Mot de Passe** : Protégez l'accès à des notes spécifiques avec un mot de passe.
44
45
  - **Recherche** : Retrouvez du contenu rapidement grâce à une fonction de recherche dans toutes vos notes.
45
46
  - **Stockage en base** : Pour l'instant les notes sont stockées en clair dans la base. A voir pour rajouter le support du chiffrement côté serveur.
@@ -92,6 +93,18 @@ Le serveur tournera en arrière-plan, en attente de connexions.
92
93
  | `--username <nom>` | Spécifie le nom d'utilisateur pour la création. | `... --username admin` |
93
94
  | `--password <mdp>` | Spécifie le mot de passe pour la création. | `... --password "un_mot_de_passe_solide"` |
94
95
 
96
+ ### Détail du fichier de config
97
+
98
+ ```ini
99
+ server:
100
+ verbose: true #Afficher les logs
101
+ host: 127.0.0.1 #Ip d'écoute
102
+ port: 40017 #Port d'écoute
103
+ key_size: 2048 #Taille de la clé RSA
104
+ key_otp: JFSWY3DPEHPK3PXP #Clé OTP pour le démarrage
105
+ data_dir: #Chemin vers la base SQLite
106
+ working_dir: #Emplacement des fichiers de l'application
107
+ ```
95
108
 
96
109
  ## Côté client
97
110
 
@@ -141,3 +154,33 @@ La syntaxe générale est `znote <commande> [arguments...]`.
141
154
  | `workspace <nom_ws>` | Change le workspace actif. | `znote workspace "Projets"` |
142
155
  | `workspace <nom_ws> --create` | Crée un nouveau workspace. | `znote workspace "Archives" --create` |
143
156
  | `workspace <nom_ws> --remove` | Supprime un workspace. | `znote workspace "Archives" --remove` |
157
+
158
+
159
+ ### Commandes des Channels
160
+
161
+ La fonctionnalité de "channel" permet de synchroniser des fichiers entre plusieurs postes.
162
+
163
+ | Commande | Description | Exemple d’utilisation |
164
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
165
+ | `channel` | Liste tous les channels disponibles sur le serveur. | `znote channel` |
166
+ | `channel <nom> --detail` | Affiche les informations détaillées d'un channel (hash, date de modification...). | `znote channel mon-canal --detail` |
167
+ | `channel <nom> --tree` | Affiche l'arborescence et la liste des fichiers contenus dans le channel. | `znote channel mon-canal --tree` |
168
+ | `publish <channel> <fichier>` | Publie un fichier sur un channel. Si le fichier existe déjà, il est mis à jour. | `znote publish mon-canal ./rapport.pdf` |
169
+ | `publish <channel> --purge` | Supprime les fichiers du channel qui ne sont pas présents dans la publication actuelle. | `znote publish mon-canal --purge` |
170
+ | `fetch <channel> (<path>)` | Récupère (télécharge) tous les fichiers d'un channel dans le répertoire local. | `znote fetch mon-canal` |
171
+ | `fetch <channel> --force` | Force le téléchargement et l'écrasement des fichiers locaux même s'ils existent déjà. | `znote fetch mon-canal --force` |
172
+ | `fetch <channel> --purge` | Supprime les fichiers locaux qui ne sont pas présent dans le channel. | `znote fetch mon-canal --purge` |
173
+ | `diff <channel>` | Compare le répertoire local avec le contenu du channel et affiche les différences (fichiers nouveaux, modifiés ou à supprimer). | `znote diff mon-canal` |
174
+
175
+
176
+ ### Détail du fichier de config
177
+
178
+ ```ini
179
+ client:
180
+ verbose: true #Afficher les logs
181
+ host: 127.0.0.1 #Ip du serveur
182
+ port: 40017 #Port du serveur
183
+ key_size: 2048 #Taille de la clé RSA
184
+ key_otp: JFSWY3DPEHPK3PXP #Clé OTP pour le démarrage
185
+ working_dir: #Emplacement des fichiers de l'application
186
+ ```
@@ -11,6 +11,7 @@ znote est une application de prise de notes en ligne de commande fonctionnant su
11
11
  - **Éditeur Intégré** : Un éditeur de texte simple est intégré directement dans le terminal pour une expérience fluide.
12
12
  - **Workspaces (Espaces de travail)** : Organisez vos notes dans des espaces de travail distincts.
13
13
  - **Synchronisation de Fichiers** : Envoyez (`push`) un fichier local pour créer une note, ou téléchargez (`pull`) une note dans un fichier.
14
+ - **Channels de Diffusion** : Partagez et synchronisez des fichiers sur plusieurs postes via des "channels".
14
15
  - **Protection par Mot de Passe** : Protégez l'accès à des notes spécifiques avec un mot de passe.
15
16
  - **Recherche** : Retrouvez du contenu rapidement grâce à une fonction de recherche dans toutes vos notes.
16
17
  - **Stockage en base** : Pour l'instant les notes sont stockées en clair dans la base. A voir pour rajouter le support du chiffrement côté serveur.
@@ -63,6 +64,18 @@ Le serveur tournera en arrière-plan, en attente de connexions.
63
64
  | `--username <nom>` | Spécifie le nom d'utilisateur pour la création. | `... --username admin` |
64
65
  | `--password <mdp>` | Spécifie le mot de passe pour la création. | `... --password "un_mot_de_passe_solide"` |
65
66
 
67
+ ### Détail du fichier de config
68
+
69
+ ```ini
70
+ server:
71
+ verbose: true #Afficher les logs
72
+ host: 127.0.0.1 #Ip d'écoute
73
+ port: 40017 #Port d'écoute
74
+ key_size: 2048 #Taille de la clé RSA
75
+ key_otp: JFSWY3DPEHPK3PXP #Clé OTP pour le démarrage
76
+ data_dir: #Chemin vers la base SQLite
77
+ working_dir: #Emplacement des fichiers de l'application
78
+ ```
66
79
 
67
80
  ## Côté client
68
81
 
@@ -112,3 +125,33 @@ La syntaxe générale est `znote <commande> [arguments...]`.
112
125
  | `workspace <nom_ws>` | Change le workspace actif. | `znote workspace "Projets"` |
113
126
  | `workspace <nom_ws> --create` | Crée un nouveau workspace. | `znote workspace "Archives" --create` |
114
127
  | `workspace <nom_ws> --remove` | Supprime un workspace. | `znote workspace "Archives" --remove` |
128
+
129
+
130
+ ### Commandes des Channels
131
+
132
+ La fonctionnalité de "channel" permet de synchroniser des fichiers entre plusieurs postes.
133
+
134
+ | Commande | Description | Exemple d’utilisation |
135
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
136
+ | `channel` | Liste tous les channels disponibles sur le serveur. | `znote channel` |
137
+ | `channel <nom> --detail` | Affiche les informations détaillées d'un channel (hash, date de modification...). | `znote channel mon-canal --detail` |
138
+ | `channel <nom> --tree` | Affiche l'arborescence et la liste des fichiers contenus dans le channel. | `znote channel mon-canal --tree` |
139
+ | `publish <channel> <fichier>` | Publie un fichier sur un channel. Si le fichier existe déjà, il est mis à jour. | `znote publish mon-canal ./rapport.pdf` |
140
+ | `publish <channel> --purge` | Supprime les fichiers du channel qui ne sont pas présents dans la publication actuelle. | `znote publish mon-canal --purge` |
141
+ | `fetch <channel> (<path>)` | Récupère (télécharge) tous les fichiers d'un channel dans le répertoire local. | `znote fetch mon-canal` |
142
+ | `fetch <channel> --force` | Force le téléchargement et l'écrasement des fichiers locaux même s'ils existent déjà. | `znote fetch mon-canal --force` |
143
+ | `fetch <channel> --purge` | Supprime les fichiers locaux qui ne sont pas présent dans le channel. | `znote fetch mon-canal --purge` |
144
+ | `diff <channel>` | Compare le répertoire local avec le contenu du channel et affiche les différences (fichiers nouveaux, modifiés ou à supprimer). | `znote diff mon-canal` |
145
+
146
+
147
+ ### Détail du fichier de config
148
+
149
+ ```ini
150
+ client:
151
+ verbose: true #Afficher les logs
152
+ host: 127.0.0.1 #Ip du serveur
153
+ port: 40017 #Port du serveur
154
+ key_size: 2048 #Taille de la clé RSA
155
+ key_otp: JFSWY3DPEHPK3PXP #Clé OTP pour le démarrage
156
+ working_dir: #Emplacement des fichiers de l'application
157
+ ```
@@ -90,4 +90,75 @@ def RestoreScreen():
90
90
 
91
91
  def CleanScreen():
92
92
  sys.stdout.write("\033[2J") # Effacer l'écran
93
- sys.stdout.flush()
93
+ sys.stdout.flush()
94
+
95
+
96
+ #Affichage arborescence en mode tree
97
+ @impmagic.loader(
98
+ {'module': 'collections', 'submodule': ['defaultdict']},
99
+ )
100
+ def build_tree(paths):
101
+ tree = lambda: defaultdict(tree)
102
+ root = tree()
103
+
104
+ for path in paths:
105
+ parts = path.strip(".\\/").replace("/", "\\").split("\\")
106
+ current = root
107
+ for i, part in enumerate(parts):
108
+ if i == len(parts) - 1:
109
+ current[part] = "FILE"
110
+ else:
111
+ current = current[part]
112
+ return root
113
+
114
+ # Tree builder for dict with statuses
115
+ @impmagic.loader(
116
+ {'module': 'collections', 'submodule': ['defaultdict']},
117
+ )
118
+ def build_tree_from_dict(status_map):
119
+ tree = lambda: defaultdict(tree)
120
+ root = tree()
121
+ status_lookup = {}
122
+
123
+ for path, status in status_map.items():
124
+ parts = path.strip(".\\/").replace("/", "\\").split("\\")
125
+ current = root
126
+ for i, part in enumerate(parts):
127
+ if i == len(parts) - 1:
128
+ current[part] = status # store status as a leaf value
129
+ else:
130
+ current = current[part]
131
+ return root
132
+
133
+
134
+ STATUS_COLOR = {
135
+ "OK": "cyan", # green
136
+ "UPDATE": "light_yellow", # yellow
137
+ "DELETE": "gold_3b", # red
138
+ "NEW": "magenta_3a", # cyan
139
+ "FOLDER": "grey_53", # blue for directories
140
+ "RESET": "\033[0m",
141
+ }
142
+
143
+ # Unified print function
144
+ @impmagic.loader(
145
+ {'module': 'zpp_color', 'submodule': ['fg', 'attr']},
146
+ )
147
+ def print_tree(d, prefix=""):
148
+ keys = sorted(d.keys())
149
+ for i, key in enumerate(keys):
150
+ is_last = i == len(keys) - 1
151
+ connector = "└── " if is_last else "├── "
152
+ value = d[key]
153
+
154
+
155
+ if isinstance(value, dict):
156
+ # Directory
157
+ color = STATUS_COLOR["FOLDER"]
158
+ print(f"{prefix}{connector}{fg(color)}{key}{attr(0)}")
159
+ new_prefix = prefix + (" " if is_last else "│ ")
160
+ print_tree(value, new_prefix)
161
+ else:
162
+ # File with status
163
+ color = STATUS_COLOR.get(value, "cyan")
164
+ print(f"{prefix}{connector}{fg(color)}{key}{attr(0)}")
@@ -181,6 +181,33 @@ class Client():
181
181
  return ""
182
182
 
183
183
 
184
+ @impmagic.loader(
185
+ {'module': '__main__'},
186
+ {'module': 'hashlib'},
187
+ {'module': 'app.display', 'submodule': ['logs', 'notify']},
188
+ )
189
+ def publish_file(self, file, expanded_path, channel_name):
190
+ logs(f"Envoi de {os.path.relpath(file, expanded_path)}")
191
+ with open(file, 'rb') as f:
192
+ content = f.read()
193
+ content_hash = hashlib.sha256(content).hexdigest()
194
+
195
+ command = {
196
+ 'method': 'channel',
197
+ 'action': 'publish',
198
+ 'channel_name': channel_name,
199
+ 'name': os.path.relpath(file, expanded_path),
200
+ 'content': content,
201
+ 'hash': content_hash,
202
+ 'token': __main__.settings.token
203
+ }
204
+
205
+ message = self.call_server(command)
206
+ if message['method']=="notify":
207
+ if 'message' in message and 'status_code' in message:
208
+ notify(message['message'], message['status_code'])
209
+
210
+
184
211
  #Envoie une commande au serveur et attends une réponse
185
212
  @impmagic.loader(
186
213
  {'module': '__main__'},
@@ -225,6 +252,7 @@ class Client():
225
252
  {'module': 'json'},
226
253
  {'module': 'os'},
227
254
  {'module': 'hashlib'},
255
+ {'module': 'glob', 'submodule': ['glob']},
228
256
  {'module': 'core.editor', 'submodule': ['Editor']},
229
257
  {'module': 'app.display', 'submodule': ['logs', 'notify', 'print_nxs', 'SaveScreen', 'RestoreScreen']},
230
258
  {'module': 'core.pack', 'submodule': ['pack']},
@@ -397,13 +425,90 @@ class Client():
397
425
  'token': __main__.settings.token
398
426
  }
399
427
 
428
+ elif instruction=="channel":
429
+ command = {
430
+ 'method': 'channel',
431
+ 'action': 'list',
432
+ 'token': __main__.settings.token
433
+ }
434
+
435
+ if len(parameter)>1:
436
+ if argument.detail:
437
+ command['action'] = 'info'
438
+
439
+ elif argument.tree:
440
+ command['action'] = 'tree'
441
+
442
+ command['channel_name'] = parameter[1]
443
+
444
+ elif instruction=="fetch":
445
+ if len(parameter)>1:
446
+ command = {
447
+ 'method': 'channel',
448
+ 'action': 'fetch',
449
+ 'channel_name': parameter[1],
450
+ 'token': __main__.settings.token
451
+ }
452
+
453
+ if len(parameter)>2:
454
+ command['path'] = parameter[2]
455
+
456
+ if argument.force:
457
+ command['force'] = True
458
+ else:
459
+ command['force'] = False
460
+
461
+ if argument.purge:
462
+ command['purge'] = True
463
+ else:
464
+ command['purge'] = False
465
+
466
+ elif instruction=="diff":
467
+ if len(parameter)>1:
468
+ command = {
469
+ 'method': 'channel',
470
+ 'action': 'diff',
471
+ 'channel_name': parameter[1],
472
+ 'token': __main__.settings.token
473
+ }
474
+
475
+ if len(parameter)>2:
476
+ command['path'] = parameter[2]
477
+
478
+ elif instruction=="publish":
479
+ channel_name = parameter[1]
480
+ path = parameter[2]
481
+
482
+ expanded_path = os.path.expanduser(path)
483
+
484
+ files = glob(f"{expanded_path}/**/*", recursive=True)
485
+ #files = [os.path.relpath(p, expanded_path) for p in results]
486
+ push_file = []
487
+
488
+ for file in files:
489
+ if os.path.isfile(file):
490
+ push_file.append(os.path.relpath(file, expanded_path))
491
+ self.publish_file(file, expanded_path, channel_name)
492
+
493
+ if argument.purge:
494
+ logs("Purge du channel", "info")
495
+ command = {
496
+ 'method': 'channel',
497
+ 'action': 'purge',
498
+ 'channel_name': parameter[1],
499
+ 'list_files': push_file,
500
+ 'token': __main__.settings.token
501
+ }
502
+
400
503
  if command:
401
504
  self.send_and_response(command)
402
505
 
403
506
 
404
507
  @impmagic.loader(
405
508
  {'module': '__main__'},
406
- {'module': 'app.display', 'submodule': ['logs', 'notify', 'print_nxs']},
509
+ {'module': 'core.client.fetch', 'submodule': ['remove_unlisted_files', 'build_diff_tree']},
510
+ {'module': 'core.client.metafile', 'submodule': ['read_metafile', 'write_metafile']},
511
+ {'module': 'app.display', 'submodule': ['logs', 'notify', 'print_nxs', 'build_tree', 'print_tree']},
407
512
  {'module': 'core.pack', 'submodule': ['pack']},
408
513
  {'module': 'core.client.token', 'submodule': ['set_token']},
409
514
  {'module': 'app.input', 'submodule': ['getpass']},
@@ -458,7 +563,6 @@ class Client():
458
563
  for note in message['content']:
459
564
  print(f" - {note}")
460
565
 
461
-
462
566
  elif message['method']=="notify":
463
567
  if 'message' in message and 'status_code' in message:
464
568
  notify(message['message'], message['status_code'])
@@ -470,7 +574,104 @@ class Client():
470
574
  payload['password_hash'] = hashlib.sha256(password.encode()).hexdigest()
471
575
 
472
576
  self.send_and_response(payload)
577
+
578
+ elif message['method']=="channel":
579
+ if 'action' in message and message['action']=="fetch":
580
+ if 'tree' in message:
581
+ list_files = []
582
+ for file in message['tree']:
583
+ #Spécifie le chemin de destination
584
+ if 'path' in message:
585
+ file_path = os.path.join(message['path'], file['name'])
586
+ else:
587
+ file_path = file['name']
473
588
 
589
+ list_files.append(file['name'])
590
+
591
+ #Forcer la mise à jour du contenu
592
+ if 'force' in message:
593
+ force = message['force']
594
+ else:
595
+ force = False
596
+
597
+ #Supprimer les fichiers non présent dans le channel
598
+ if 'purge' in message:
599
+ purge = message['purge']
600
+ else:
601
+ purge = False
602
+
603
+ if not os.path.exists(os.path.dirname(file_path)):
604
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
605
+
606
+ content_hash = None
607
+ if os.path.exists(file_path):
608
+ with open(file_path, 'rb') as f:
609
+ content_hash = hashlib.sha256(f.read()).hexdigest()
610
+
611
+ if content_hash and content_hash==file['content_hash'] and not force:
612
+ pass
613
+ else:
614
+ with open(file_path, 'wb') as f:
615
+ f.write(file['content'])
616
+ if content_hash:
617
+ logs(f"{file['name']} mis à jour", "success", force=True)
618
+ else:
619
+ logs(f"{file['name']} récupéré", "success", force=True)
620
+
621
+ if purge:
622
+ if 'path' in message:
623
+ file_path = message['path']
624
+ else:
625
+ file_path = "."
626
+ expanded_path = os.path.expanduser(file_path)
627
+ list_files = [p.replace('\\', '/') for p in list_files]
628
+
629
+ remove_unlisted_files(file_path, list_files)
630
+
631
+ """
632
+ if 'channel_hash' in message:
633
+ if 'path' in message:
634
+ file_path = os.path.join(message['path'], '.znote')
635
+ else:
636
+ file_path = ".znote"
637
+
638
+ content = {
639
+ "channel_name": message['channel_name'],
640
+ "channel_hash": message['channel_hash'],
641
+ }
642
+
643
+ write_metafile(content, file_path)
644
+ """
645
+
646
+ elif 'action' in message and message['action']=="diff":
647
+ if 'tree' in message:
648
+ if 'path' in message:
649
+ file_path = message['path']
650
+ else:
651
+ file_path = "."
652
+ print(file_path)
653
+ diff_output = build_diff_tree(file_path, message["tree"])
654
+ print_tree(diff_output)
655
+
656
+
657
+ elif 'action' in message and message['action']=="list":
658
+ if 'channel_list' in message:
659
+ for channel_name in message['channel_list']:
660
+ print_nxs(" - ", color='dark_gray',nojump=True)
661
+ print_nxs(channel_name)
662
+
663
+ elif 'action' in message and message['action']=="info":
664
+ if 'channel_info' in message:
665
+ for element in ['name', 'description', 'hash', 'updated_at', 'total_files']:
666
+ if element in message['channel_info'] and message['channel_info'][element]:
667
+ print_nxs(f"{element}: ", color='dark_gray',nojump=True)
668
+ print_nxs(message['channel_info'][element])
669
+
670
+ elif 'action' in message and message['action']=="tree":
671
+ if 'list_files' in message:
672
+ tree = build_tree(message['list_files'])
673
+ print_tree(tree)
674
+
474
675
  else:
475
676
  print(message)
476
677
 
@@ -526,6 +727,10 @@ def run_client():
526
727
  parse.set_argument(longname="create", description="Création d'un workspace", default=False, category="workspace")
527
728
  parse.set_argument(longname="remove", description="Suppression d'un workspace", default=False, category="workspace")
528
729
  parse.set_argument(longname="info", description="Afficher les informations de connexion", default=False)
730
+ parse.set_argument(longname="purge", description="Purger le channel durant le publication ou durant le fetch", default=False, category="channel")
731
+ parse.set_argument(longname="force", description="Forcer le changement avec fetch", default=False, category="channel")
732
+ parse.set_argument(longname="detail", description="Afficher les informations du channel", default=False, category="channel")
733
+ parse.set_argument(longname="tree", description="Afficher l'arborescence du channel", default=False, category="channel")
529
734
  #parse.set_parameter("command", description="commande à lancer")
530
735
  parse.set_parameter("login", "Connexion à un serveur de note")
531
736
  parse.set_parameter("logout", "Déconnexion d'un serveur de note")
@@ -541,6 +746,9 @@ def run_client():
541
746
  parse.set_parameter("info", "Afficher les informations d'une note")
542
747
  parse.set_parameter("find", "Rechercher un contenu dans les notes")
543
748
  parse.set_parameter("protect", "Protéger une note par mot de passe")
749
+ parse.set_parameter("publish", "Publier du contenu sur un channel")
750
+ parse.set_parameter("fetch", "Récupérer le contenu sur un channel")
751
+ parse.set_parameter("channel", "Afficher les informations sur les channels")
544
752
  parse.disable_check()
545
753
  parameter, argument = parse.load()
546
754
 
@@ -0,0 +1,83 @@
1
+ import impmagic
2
+
3
+ @impmagic.loader(
4
+ {'module': 'os'},
5
+ {'module': 'glob'},
6
+ {'module': 'app.display', 'submodule': ['logs']},
7
+ )
8
+ def remove_unlisted_files(base_dir, paths_to_keep):
9
+ # Normaliser les chemins (en absolu)
10
+ paths_to_keep_abs = {os.path.abspath(os.path.join(base_dir, p)) for p in paths_to_keep}
11
+
12
+ # Lister tous les fichiers dans base_dir récursivement
13
+ all_files = glob.glob(os.path.join(base_dir, '**'), recursive=True)
14
+ all_files = [f for f in all_files if os.path.isfile(f)]
15
+ all_files = [p.replace('\\', '/') for p in all_files]
16
+
17
+ # Supprimer ceux qui ne sont pas dans paths_to_keep
18
+ for file_path in all_files:
19
+ abs_path = os.path.abspath(file_path)
20
+ if abs_path not in paths_to_keep_abs:
21
+ logs(f"{os.path.expanduser(file_path)} supprimé", force=True)
22
+ os.remove(abs_path)
23
+
24
+
25
+
26
+ import os
27
+ import hashlib
28
+ from collections import defaultdict
29
+ from app.display import build_tree_from_dict
30
+
31
+ def build_diff_tree(base_path, remote_tree):
32
+ local_files = {}
33
+ for root, _, files in os.walk(base_path):
34
+ for file in files:
35
+ full_path = os.path.join(root, file)
36
+ rel_path = os.path.relpath(full_path, base_path).replace('\\', '/')
37
+ with open(full_path, 'rb') as f:
38
+ local_files[rel_path] = hashlib.sha256(f.read()).hexdigest()
39
+
40
+ remote_map = {f["name"].replace('\\', '/'): f["content_hash"] for f in remote_tree}
41
+ all_paths = set(local_files) | set(remote_map)
42
+
43
+ diff = {}
44
+ for path in sorted(all_paths):
45
+ if path in local_files and path in remote_map:
46
+ if local_files[path] == remote_map[path]:
47
+ diff[path] = "OK"
48
+ else:
49
+ diff[path] = "UPDATE"
50
+ elif path in local_files:
51
+ diff[path] = "DELETE"
52
+ else:
53
+ diff[path] = "NEW"
54
+
55
+ return build_tree_from_dict(diff)
56
+
57
+ def build_tree_view(diff_map):
58
+ tree = defaultdict(list)
59
+ for path, status in diff_map.items():
60
+ parts = path.split('/')
61
+ for i in range(1, len(parts)):
62
+ folder = '/'.join(parts[:i])
63
+ tree[folder].append(None)
64
+ tree[path] = status
65
+
66
+ printed = set()
67
+ lines = []
68
+
69
+ def print_branch(prefix, path_parts):
70
+ full_path = '/'.join(path_parts)
71
+ entries = {k for k in tree if k.startswith(full_path + '/') and k.count('/') == len(path_parts)}
72
+ files = sorted(e for e in entries if tree[e] != None)
73
+ folders = sorted(e for e in entries if tree[e] == None)
74
+ for i, item in enumerate(folders + files):
75
+ connector = '└── ' if i == len(folders + files) - 1 else '├── '
76
+ label = item.split('/')[-1]
77
+ status = f" [{tree[item]}]" if tree[item] else ''
78
+ lines.append(f"{prefix}{connector}{label}{status}")
79
+ if tree[item] is None:
80
+ print_branch(prefix + (' ' if i == len(folders + files) - 1 else '│ '), item.split('/'))
81
+
82
+ print_branch("", [])
83
+ return '\n'.join(lines)
@@ -0,0 +1,12 @@
1
+ import yaml
2
+
3
+ def read_metafile(path):
4
+ if os.path.exists(path):
5
+ with open(path, 'r') as file:
6
+ content = yaml.safe_load(file)
7
+ return content
8
+ return {}
9
+
10
+ def write_metafile(content, path):
11
+ with open(path, 'w') as file:
12
+ yaml.dump(content, file)
@@ -14,12 +14,12 @@ class Settings():
14
14
  {'module': 'core.client.token','submodule': ['get_token']}
15
15
  )
16
16
  def __init__(self, config_file):
17
- self.config_dir = os.path.abspath(os.path.expanduser('~/.config/znote'))
18
-
19
- self.token_file = os.path.join(self.config_dir, ".token")
20
-
21
17
  if not config_file:
22
- config_file = os.path.join(self.config_dir, "config.yml")
18
+ config_dir = os.path.abspath(os.path.expanduser('~/.config/znote'))
19
+ config_file = os.path.join(config_dir, "config.yml")
20
+
21
+ if not os.path.exists(os.path.dirname(config_file)):
22
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
23
23
 
24
24
  self.config = Config(f"{config_file}")
25
25
 
@@ -28,8 +28,12 @@ class Settings():
28
28
  self.host = self.config.get('client.host', default="127.0.0.1", auto_set=True)
29
29
  self.port = self.config.get('client.port', default=40017, auto_set=True)
30
30
  self.key_size = self.config.get('client.key_size', default=2048, auto_set=True)
31
- self.key_otp = self.config.get('client.key_otp', default='JBSWY3DPEHPK3PXP', auto_set=True)
31
+ self.key_otp = self.config.get('client.key_otp', default='JBSWY3DPEHPK3PXP', auto_set=True)
32
+ self.config_dir = self.config.get('server.working_dir', default=os.path.abspath(os.path.expanduser('~/.config/znote')), auto_set=True)
33
+
32
34
  self.totp = pyotp.TOTP(self.key_otp)
35
+
36
+ self.token_file = os.path.join(self.config_dir, ".token")
33
37
 
34
38
  self.PubServ = None
35
39
  self.keyC = None
@@ -123,6 +127,7 @@ class Settings():
123
127
  port = input(f"{fg('dark_gray')}port [{fg('cyan')}{self.port}{fg('dark_gray')}]: ")
124
128
  key_size = input(f"{fg('dark_gray')}key_size [{fg('cyan')}{self.key_size}{fg('dark_gray')}]: ")
125
129
  key_otp = input(f"{fg('dark_gray')}key_otp [{fg('cyan')}{self.key_otp}{fg('dark_gray')}]: ")
130
+ working_dir = input(f"{fg('dark_gray')}working_dir [{fg('cyan')}{self.working_dir}{fg('dark_gray')}]: ")
126
131
  verbose = input(f"{fg('dark_gray')}verbose [{fg('cyan')}{self.verbose}{fg('dark_gray')}]: ")
127
132
 
128
133
  if host:
@@ -137,6 +142,9 @@ class Settings():
137
142
  if key_otp:
138
143
  self.config.set('server.key_otp', key_otp)
139
144
 
145
+ if working_dir:
146
+ self.config.set('server.working_dir', working_dir)
147
+
140
148
  if verbose:
141
149
  self.config.set('server.verbose', verbose)
142
150
 
@@ -10,14 +10,12 @@ class Settings:
10
10
  {'module': 'core.config', 'submodule': ['Config']},
11
11
  )
12
12
  def __init__(self, config_file=None):
13
- self.config_dir = os.path.abspath(os.path.expanduser('~/.config/znote'))
14
-
15
- if not os.path.exists(self.config_dir):
16
- os.makedirs(self.config_dir, exist_ok=True)
17
-
18
13
  if not config_file:
19
- config_file = os.path.join(self.config_dir, "config.yml")
20
- #config_file = "config.toml"
14
+ config_dir = os.path.abspath(os.path.expanduser('~/.config/znote'))
15
+ config_file = os.path.join(config_dir, "config.yml")
16
+
17
+ if not os.path.exists(os.path.dirname(config_file)):
18
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
21
19
 
22
20
  self.config = Config(config_file)
23
21
 
@@ -27,6 +25,7 @@ class Settings:
27
25
  self.port = self.config.get('server.port', default=40017, auto_set=True)
28
26
  self.key_size = self.config.get('server.key_size', default=2048, auto_set=True)
29
27
  self.key_otp = self.config.get('server.key_otp', default='JBSWY3DPEHPK3PXP', auto_set=True)
28
+ self.config_dir = self.config.get('server.working_dir', default=os.path.abspath(os.path.expanduser('~/.config/znote')), auto_set=True)
30
29
 
31
30
  #Initialisation de l'OTP pour la connexion initiale
32
31
  if not re.match(r'^[A-Z0-9]{16}$', self.key_otp):
@@ -36,7 +35,9 @@ class Settings:
36
35
 
37
36
  self.data_dir = self.config.get('server.data_dir', default=None, auto_set=False)
38
37
 
39
- if not self.data_dir:
38
+ if self.data_dir:
39
+ self.data_dir = os.path.join(self.config_dir, self.data_dir)
40
+ else:
40
41
  self.data_dir = os.path.join(self.config_dir, "znote.db")
41
42
 
42
43
 
@@ -127,6 +128,7 @@ class Settings:
127
128
  key_size = input(f"{fg('dark_gray')}key_size [{fg('cyan')}{self.key_size}{fg('dark_gray')}]: ")
128
129
  key_otp = input(f"{fg('dark_gray')}key_otp [{fg('cyan')}{self.key_otp}{fg('dark_gray')}]: ")
129
130
  data_dir = input(f"{fg('dark_gray')}data_dir [{fg('cyan')}{self.data_dir}{fg('dark_gray')}]: ")
131
+ working_dir = input(f"{fg('dark_gray')}working_dir [{fg('cyan')}{self.working_dir}{fg('dark_gray')}]: ")
130
132
  verbose = input(f"{fg('dark_gray')}verbose [{fg('cyan')}{self.verbose}{fg('dark_gray')}]: ")
131
133
 
132
134
  if host:
@@ -144,5 +146,8 @@ class Settings:
144
146
  if data_dir:
145
147
  self.config.set('server.data_dir', data_dir)
146
148
 
149
+ if working_dir:
150
+ self.config.set('server.working_dir', working_dir)
151
+
147
152
  if verbose:
148
153
  self.config.set('server.verbose', verbose)
@@ -22,4 +22,20 @@ def Table(db, type_db="sqlite"):
22
22
  editor_id = Required(int)
23
23
  protected = Optional(str)
24
24
  shared_with = Optional(str) # Liste d’IDs ou noms d’utilisateurs
25
- workspace = Required(str)
25
+ workspace = Required(str)
26
+
27
+ class Channel(db.Entity):
28
+ id = PrimaryKey(int, auto=True)
29
+ name = Required(str, unique=True) # Nom du canal
30
+ description = Optional(str)
31
+ channel_hash = Required(str) # Dernier hash publié
32
+ updated_at = Required(datetime)
33
+ creator_id = Required(int) # ID de l'utilisateur qui a créé le canal
34
+
35
+ class PubChannel(db.Entity):
36
+ id = PrimaryKey(int, auto=True)
37
+ channel_id = Required(int) # ID du canal (référence manuelle)
38
+ name = Required(str)
39
+ content = Required(str)
40
+ content_hash = Required(str)
41
+ updated_at = Required(datetime)
@@ -4,7 +4,13 @@ import impmagic
4
4
  import threading
5
5
 
6
6
 
7
-
7
+ @impmagic.loader(
8
+ {'module': 'uuid'},
9
+ {'module': 'hashlib'},
10
+ )
11
+ def generate_unique_hash() -> str:
12
+ unique_id = uuid.uuid4().hex
13
+ return hashlib.sha256(unique_id.encode('utf-8')).hexdigest()
8
14
 
9
15
 
10
16
  #Supprime le nom d'utilisateur de la liste lorsqu'il se déconnecte
@@ -187,6 +193,7 @@ class ThreadClient(threading.Thread):
187
193
  {'module': 'json'},
188
194
  {'module': 'hashlib'},
189
195
  {'module': 'traceback'},
196
+ {'module': 'base64'},
190
197
  {'module': 'core.pack', 'submodule': ['pack', 'unpack']},
191
198
  {'module': 'app.display', 'submodule': ['logs']},
192
199
  {'module': 'datetime', 'submodule': ['datetime']},
@@ -548,6 +555,162 @@ class ThreadClient(threading.Thread):
548
555
  payload = {'method': 'find', 'content': note_list}
549
556
  else:
550
557
  payload = {'method': 'notify', 'message': "Aucune note trouvée", 'status_code': "error"}
558
+
559
+ elif message['method']=="channel":
560
+ with pony.db_session:
561
+ if 'action' in message:
562
+ if message['action']=="publish":
563
+ for deps in ['channel_name', 'name', 'content', 'hash']:
564
+ if deps not in message:
565
+ logs(f"[{self.get_name_user()}] - {deps} manquant dans la demande", "error")
566
+ payload = {'method': 'notify', 'message': f'{deps} manquant dans la demande', 'status_code': 'error'}
567
+ break
568
+ else:
569
+ #Convertis en base64 pour stockage en base
570
+ message['content'] = base64.b64encode(message['content']).decode("utf-8")
571
+
572
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
573
+ if channel:
574
+ channel_id = channel.id
575
+ else:
576
+ logs(f"[{self.get_name_user()}] - Création du channel {message['channel_name']}", "success")
577
+ __main__.settings.orm.Channel(name=message['channel_name'], channel_hash=generate_unique_hash(), updated_at=datetime.now(), creator_id=self.user_id)
578
+ pony.commit()
579
+
580
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
581
+ channel_id = channel.id
582
+
583
+ pub_content = __main__.settings.orm.PubChannel.get(channel_id=channel_id , name=message['name'])
584
+
585
+ if pub_content:
586
+ if pub_content.content_hash!=message['hash']:
587
+ pub_content.content = message['content']
588
+ pub_content.content_hash = message['hash']
589
+ pub_content.updated_at=datetime.now()
590
+
591
+ channel.updated_at=datetime.now()
592
+ channel.channel_hash=generate_unique_hash()
593
+
594
+ logs(f"[{self.get_name_user()}] - channel: {message['channel_name']} - {message['name']} mis à jour", "success")
595
+ payload = {'method': 'notify', 'message': f"{message['name']} mis à jour", 'status_code': 'success'}
596
+ else:
597
+ logs(f"[{self.get_name_user()}] - channel: {message['channel_name']} - {message['name']} à jour", "info")
598
+ payload = {'method': 'notify', 'message': f"{message['name']} à jour", 'status_code': 'info'}
599
+
600
+ else:
601
+ publish_file = __main__.settings.orm.PubChannel(channel_id=channel_id , name=message['name'], content=message['content'], content_hash=message['hash'], updated_at=datetime.now())
602
+
603
+ channel.updated_at=datetime.now()
604
+ channel.channel_hash=generate_unique_hash()
605
+
606
+ logs(f"[{self.get_name_user()}] - channel: {message['channel_name']} - {message['name']} publié", "success")
607
+ payload = {'method': 'notify', 'message': f"{message['name']} publié", 'status_code': 'success'}
608
+
609
+
610
+
611
+ pony.commit()
612
+
613
+ elif message['action']=="purge":
614
+ if 'list_files' in message:
615
+ logs(f"[{self.get_name_user()}] - channel: {message['channel_name']} - purge du channel", "info")
616
+
617
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
618
+ channel_id = channel.id
619
+
620
+ query = __main__.settings.orm.PubChannel.select(lambda p: p.channel_id == channel_id)
621
+ result = query[:]
622
+
623
+ for file in result:
624
+ if file.name not in message['list_files']:
625
+ logs(f"[{self.get_name_user()}] - channel: {message['channel_name']} - {file.name} obsolète", "info")
626
+ file.delete()
627
+
628
+ pony.commit()
629
+
630
+ payload = {'method': 'notify', 'message': f"channel {message['channel_name']} purgé", 'status_code': 'success'}
631
+
632
+
633
+ elif message['action']=="fetch_hash":
634
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
635
+ if channel:
636
+ payload = {'method': 'channel', 'action': 'fetch_hash', 'channel_hash': channel.channel_hash}
637
+ else:
638
+ payload = {'method': 'notify', 'message': f"channel {message['channel_name']} introuvable", 'status_code': 'error'}
639
+
640
+
641
+ elif message['action']=="fetch" or message['action']=="diff":
642
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
643
+ if channel:
644
+ channel_id = channel.id
645
+ query = __main__.settings.orm.PubChannel.select(lambda p: p.channel_id == channel_id)
646
+ result = query[:]
647
+
648
+ tree = []
649
+
650
+ for file in result:
651
+ tree.append({
652
+ "name": file.name,
653
+ "content": base64.b64decode(file.content.encode("utf-8")),
654
+ "content_hash": file.content_hash,
655
+ })
656
+
657
+ payload = {'method': 'channel', 'action': message['action'], 'tree': tree, 'channel_hash': channel.channel_hash, 'channel_name': message['channel_name']}
658
+
659
+ for element in ['path', 'force', 'purge']:
660
+ if element in message:
661
+ payload[element] = message[element]
662
+
663
+ else:
664
+ payload = {'method': 'notify', 'message': f"channel {message['channel_name']} introuvable", 'status_code': 'error'}
665
+
666
+ elif message['action']=="list":
667
+ query = __main__.settings.orm.Channel.select()
668
+ result = query[:]
669
+
670
+ channel_list = []
671
+ for chan in result:
672
+ channel_list.append(chan.name)
673
+
674
+ payload = {'method': 'channel', 'action': 'list', 'channel_list': channel_list}
675
+
676
+ elif message['action']=="info":
677
+ if "channel_name" in message:
678
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
679
+ if channel:
680
+ channel_info = {
681
+ "name": channel.name,
682
+ "description": channel.description,
683
+ "hash": channel.channel_hash,
684
+ "updated_at": channel.updated_at
685
+ }
686
+
687
+ channel_id = channel.id
688
+ query = __main__.settings.orm.PubChannel.select(lambda p: p.channel_id == channel_id)
689
+ result = query[:]
690
+ channel_info["total_files"] = len(result)
691
+
692
+ payload = {'method': 'channel', 'action': 'info', 'channel_info': channel_info}
693
+
694
+ else:
695
+ payload = {'method': 'notify', 'message': f"channel {message['channel_name']} introuvable", 'status_code': 'error'}
696
+
697
+ elif message['action']=="tree":
698
+ if "channel_name" in message:
699
+ channel = __main__.settings.orm.Channel.get(name=message['channel_name'])
700
+ if channel:
701
+ channel_id = channel.id
702
+ query = __main__.settings.orm.PubChannel.select(lambda p: p.channel_id == channel_id)
703
+ result = query[:]
704
+
705
+ list_files = []
706
+ for file in result:
707
+ list_files.append(file.name)
708
+
709
+ payload = {'method': 'channel', 'action': 'tree', 'list_files': list_files}
710
+
711
+ else:
712
+ payload = {'method': 'notify', 'message': f"channel {message['channel_name']} introuvable", 'status_code': 'error'}
713
+
551
714
 
552
715
  else:
553
716
  logs(f"[{self.get_name_user()}] - Commande reçu >> {message['method']}", "info")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: znote
3
- Version: 1.0.1
3
+ Version: 1.2.3
4
4
  Summary: Vault de prise de note
5
5
  Author:
6
6
  License: mit
@@ -40,6 +40,7 @@ znote est une application de prise de notes en ligne de commande fonctionnant su
40
40
  - **Éditeur Intégré** : Un éditeur de texte simple est intégré directement dans le terminal pour une expérience fluide.
41
41
  - **Workspaces (Espaces de travail)** : Organisez vos notes dans des espaces de travail distincts.
42
42
  - **Synchronisation de Fichiers** : Envoyez (`push`) un fichier local pour créer une note, ou téléchargez (`pull`) une note dans un fichier.
43
+ - **Channels de Diffusion** : Partagez et synchronisez des fichiers sur plusieurs postes via des "channels".
43
44
  - **Protection par Mot de Passe** : Protégez l'accès à des notes spécifiques avec un mot de passe.
44
45
  - **Recherche** : Retrouvez du contenu rapidement grâce à une fonction de recherche dans toutes vos notes.
45
46
  - **Stockage en base** : Pour l'instant les notes sont stockées en clair dans la base. A voir pour rajouter le support du chiffrement côté serveur.
@@ -92,6 +93,18 @@ Le serveur tournera en arrière-plan, en attente de connexions.
92
93
  | `--username <nom>` | Spécifie le nom d'utilisateur pour la création. | `... --username admin` |
93
94
  | `--password <mdp>` | Spécifie le mot de passe pour la création. | `... --password "un_mot_de_passe_solide"` |
94
95
 
96
+ ### Détail du fichier de config
97
+
98
+ ```ini
99
+ server:
100
+ verbose: true #Afficher les logs
101
+ host: 127.0.0.1 #Ip d'écoute
102
+ port: 40017 #Port d'écoute
103
+ key_size: 2048 #Taille de la clé RSA
104
+ key_otp: JFSWY3DPEHPK3PXP #Clé OTP pour le démarrage
105
+ data_dir: #Chemin vers la base SQLite
106
+ working_dir: #Emplacement des fichiers de l'application
107
+ ```
95
108
 
96
109
  ## Côté client
97
110
 
@@ -141,3 +154,33 @@ La syntaxe générale est `znote <commande> [arguments...]`.
141
154
  | `workspace <nom_ws>` | Change le workspace actif. | `znote workspace "Projets"` |
142
155
  | `workspace <nom_ws> --create` | Crée un nouveau workspace. | `znote workspace "Archives" --create` |
143
156
  | `workspace <nom_ws> --remove` | Supprime un workspace. | `znote workspace "Archives" --remove` |
157
+
158
+
159
+ ### Commandes des Channels
160
+
161
+ La fonctionnalité de "channel" permet de synchroniser des fichiers entre plusieurs postes.
162
+
163
+ | Commande | Description | Exemple d’utilisation |
164
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
165
+ | `channel` | Liste tous les channels disponibles sur le serveur. | `znote channel` |
166
+ | `channel <nom> --detail` | Affiche les informations détaillées d'un channel (hash, date de modification...). | `znote channel mon-canal --detail` |
167
+ | `channel <nom> --tree` | Affiche l'arborescence et la liste des fichiers contenus dans le channel. | `znote channel mon-canal --tree` |
168
+ | `publish <channel> <fichier>` | Publie un fichier sur un channel. Si le fichier existe déjà, il est mis à jour. | `znote publish mon-canal ./rapport.pdf` |
169
+ | `publish <channel> --purge` | Supprime les fichiers du channel qui ne sont pas présents dans la publication actuelle. | `znote publish mon-canal --purge` |
170
+ | `fetch <channel> (<path>)` | Récupère (télécharge) tous les fichiers d'un channel dans le répertoire local. | `znote fetch mon-canal` |
171
+ | `fetch <channel> --force` | Force le téléchargement et l'écrasement des fichiers locaux même s'ils existent déjà. | `znote fetch mon-canal --force` |
172
+ | `fetch <channel> --purge` | Supprime les fichiers locaux qui ne sont pas présent dans le channel. | `znote fetch mon-canal --purge` |
173
+ | `diff <channel>` | Compare le répertoire local avec le contenu du channel et affiche les différences (fichiers nouveaux, modifiés ou à supprimer). | `znote diff mon-canal` |
174
+
175
+
176
+ ### Détail du fichier de config
177
+
178
+ ```ini
179
+ client:
180
+ verbose: true #Afficher les logs
181
+ host: 127.0.0.1 #Ip du serveur
182
+ port: 40017 #Port du serveur
183
+ key_size: 2048 #Taille de la clé RSA
184
+ key_otp: JFSWY3DPEHPK3PXP #Clé OTP pour le démarrage
185
+ working_dir: #Emplacement des fichiers de l'application
186
+ ```
@@ -19,6 +19,8 @@ znote/core/gen.py
19
19
  znote/core/pack.py
20
20
  znote/core/workspace.py
21
21
  znote/core/client/connect.py
22
+ znote/core/client/fetch.py
23
+ znote/core/client/metafile.py
22
24
  znote/core/client/settings.py
23
25
  znote/core/client/token.py
24
26
  znote/core/server/note.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes