pyzbrowser 1.0.0__py3-none-any.whl

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.
pyzbrowser/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ from flask import Flask
2
+ import os
3
+
4
+
5
+ app = Flask(__name__)
6
+
7
+ from pyzbrowser.routes import (
8
+ browse,
9
+ create_folder,
10
+ upload_files,
11
+ delete_item,
12
+ view_file,
13
+ download_file,
14
+ api_search,
15
+ download_selected,
16
+ download_zip,
17
+ ROOT_DIRS,
18
+ )
19
+
20
+ __all__ = [
21
+ "browse",
22
+ "create_folder",
23
+ "upload_files",
24
+ "delete_item",
25
+ "view_file",
26
+ "download_file",
27
+ "api_search",
28
+ "download_selected",
29
+ "download_zip",
30
+ ]
31
+
32
+ def main():
33
+ # Verifica se pelo menos um diretório existe antes de iniciar
34
+ valid_dirs = []
35
+ for name, path in ROOT_DIRS:
36
+ if os.path.exists(path):
37
+ valid_dirs.append(name)
38
+ else:
39
+ print(f"Aviso: O diretório '{name}' ({path}) não foi encontrado.")
40
+
41
+ if not valid_dirs:
42
+ print("Erro: Nenhum diretório raiz válido foi encontrado.")
43
+ else:
44
+ print(
45
+ f"PyBrowser iniciado com {len(valid_dirs)} pasta(s) raiz: {', '.join(valid_dirs)}"
46
+ )
47
+ app.run(host="0.0.0.0", debug=True, port=5000)
pyzbrowser/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ Entry point for running PyMyFileBrowser as a module.
3
+ Usage: python -m pymyfilebrowser
4
+ """
5
+
6
+ from . import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
pyzbrowser/routes.py ADDED
@@ -0,0 +1,636 @@
1
+ import os
2
+ import socket
3
+ from datetime import datetime
4
+
5
+ import zipstream
6
+ from flask import (
7
+ Response,
8
+ abort,
9
+ redirect,
10
+ render_template,
11
+ request,
12
+ send_from_directory,
13
+ url_for,
14
+ )
15
+ from werkzeug.utils import secure_filename
16
+
17
+ from pyzbrowser import app
18
+ from pyzbrowser.utils import (
19
+ get_file_icon,
20
+ get_root_dir,
21
+ get_root_name_from_path,
22
+ load_root_dirs,
23
+ remove_root_from_path,
24
+ )
25
+
26
+ ROOT_DIRS = load_root_dirs()
27
+
28
+
29
+ @app.route("/")
30
+ @app.route("/browse/<path:subpath>")
31
+ def browse(subpath=""):
32
+ hostname = socket.gethostname()
33
+
34
+ # Se não há subpath, mostrar as pastas raiz disponíveis
35
+ if not subpath:
36
+ items = []
37
+ for root_name, root_path in ROOT_DIRS:
38
+ if os.path.exists(root_path):
39
+ try:
40
+ created_time = os.path.getctime(root_path)
41
+ created_date = datetime.fromtimestamp(created_time).strftime(
42
+ "%d/%m/%Y %H:%M"
43
+ )
44
+ except Exception:
45
+ created_time = 0
46
+ created_date = "Desconhecida"
47
+
48
+ items.append(
49
+ {
50
+ "name": root_name,
51
+ "path": root_name,
52
+ "is_dir": True,
53
+ "icon": "📁",
54
+ "created": created_time,
55
+ "created_date": created_date,
56
+ "ext": "",
57
+ "is_viewable": False,
58
+ }
59
+ )
60
+
61
+ return render_template(
62
+ "index.html",
63
+ items=items,
64
+ current_path="",
65
+ parent_path="",
66
+ hostname=hostname,
67
+ search_query="",
68
+ sort_by="name",
69
+ )
70
+
71
+ # Extrair o nome da raiz do path
72
+ root_name = get_root_name_from_path(subpath)
73
+ if not root_name:
74
+ abort(404)
75
+
76
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
77
+ if not ROOT_DIR:
78
+ abort(404)
79
+
80
+ # Remover o nome da raiz do subpath para obter o caminho relativo
81
+ relative_path = remove_root_from_path(subpath)
82
+
83
+ # Segurança: Evita que o usuário suba níveis com ".."
84
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
85
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
86
+ abort(403)
87
+
88
+ if not os.path.exists(full_path):
89
+ abort(404)
90
+
91
+ # Se for arquivo, faz o download direto
92
+ if os.path.isfile(full_path):
93
+ return send_from_directory(
94
+ os.path.dirname(full_path), os.path.basename(full_path)
95
+ )
96
+
97
+ # Obter parâmetros de busca e ordenação
98
+ search_query = request.args.get("search", "").lower().strip()
99
+ sort_by = request.args.get("sort", "name") # name, date, type
100
+
101
+ # Listagem de diretório
102
+ items = []
103
+ try:
104
+ for name in os.listdir(full_path):
105
+ # Filtrar por busca
106
+ if search_query and search_query not in name.lower():
107
+ continue
108
+
109
+ item_full_path = os.path.join(full_path, name)
110
+ # Incluir o nome da raiz no path
111
+ if relative_path:
112
+ rel_path = f"{root_name}/{relative_path}/{name}".replace("\\", "/")
113
+ else:
114
+ rel_path = f"{root_name}/{name}".replace("\\", "/")
115
+ is_dir = os.path.isdir(item_full_path)
116
+
117
+ # Obter data de criação
118
+ try:
119
+ created_time = os.path.getctime(item_full_path)
120
+ created_date = datetime.fromtimestamp(created_time).strftime(
121
+ "%d/%m/%Y %H:%M"
122
+ )
123
+ except Exception:
124
+ created_time = 0
125
+ created_date = "Desconhecida"
126
+
127
+ ext = os.path.splitext(name)[1].lower() if not is_dir else ""
128
+
129
+ # Verificar se é um arquivo visualizável
130
+ viewable_extensions = [
131
+ # Imagens
132
+ ".jpg",
133
+ ".jpeg",
134
+ ".png",
135
+ ".gif",
136
+ ".bmp",
137
+ ".svg",
138
+ ".webp",
139
+ ".ico",
140
+ # Vídeos
141
+ ".mp4",
142
+ ".webm",
143
+ ".ogg",
144
+ # Áudio
145
+ ".mp3",
146
+ ".wav",
147
+ ".ogg",
148
+ ".m4a",
149
+ # Documentos
150
+ ".pdf",
151
+ # Texto/Código
152
+ ".txt",
153
+ ".md",
154
+ ".log",
155
+ ".py",
156
+ ".js",
157
+ ".jsx",
158
+ ".ts",
159
+ ".tsx",
160
+ ".ini",
161
+ ".html",
162
+ ".htm",
163
+ ".css",
164
+ ".scss",
165
+ ".sass",
166
+ ".json",
167
+ ".xml",
168
+ ".yaml",
169
+ ".yml",
170
+ ".c",
171
+ ".cpp",
172
+ ".h",
173
+ ".hpp",
174
+ ".java",
175
+ ".php",
176
+ ".rb",
177
+ ".go",
178
+ ".rs",
179
+ ".sh",
180
+ ".bat",
181
+ ]
182
+ is_viewable = ext in viewable_extensions
183
+
184
+ items.append(
185
+ {
186
+ "name": name,
187
+ "path": rel_path,
188
+ "is_dir": is_dir,
189
+ "icon": get_file_icon(name, is_dir),
190
+ "created": created_time,
191
+ "created_date": created_date,
192
+ "ext": ext,
193
+ "is_viewable": is_viewable,
194
+ }
195
+ )
196
+ except PermissionError:
197
+ abort(403)
198
+
199
+ # Ordenar itens
200
+ if sort_by == "name":
201
+ items.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
202
+ elif sort_by == "date":
203
+ items.sort(key=lambda x: (not x["is_dir"], -x["created"]))
204
+ elif sort_by == "type":
205
+ items.sort(key=lambda x: (not x["is_dir"], x["ext"], x["name"].lower()))
206
+
207
+ # Calcular o caminho pai para o botão "Voltar"
208
+ parent_path = ""
209
+ if subpath:
210
+ parts = subpath.split("/")
211
+ if len(parts) > 1:
212
+ parent_path = "/".join(parts[:-1])
213
+ # Se só tem o nome da raiz, parent_path fica vazio (volta para home)
214
+
215
+ return render_template(
216
+ "index.html",
217
+ items=items,
218
+ current_path=subpath,
219
+ parent_path=parent_path,
220
+ hostname=hostname,
221
+ search_query=search_query,
222
+ sort_by=sort_by,
223
+ )
224
+
225
+
226
+ @app.route("/create_folder", methods=["POST"])
227
+ def create_folder():
228
+ current_path = request.form.get("path", "")
229
+ folder_name = request.form.get("folder_name", "").strip()
230
+
231
+ if not folder_name or not current_path:
232
+ abort(400)
233
+
234
+ # Segurança: limpar o nome da pasta
235
+ folder_name = secure_filename(folder_name)
236
+
237
+ # Extrair o nome da raiz
238
+ root_name = get_root_name_from_path(current_path)
239
+ if not root_name:
240
+ abort(404)
241
+
242
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
243
+ if not ROOT_DIR:
244
+ abort(404)
245
+
246
+ relative_path = remove_root_from_path(current_path)
247
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
248
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
249
+ abort(403)
250
+
251
+ new_folder_path = os.path.join(full_path, folder_name)
252
+
253
+ try:
254
+ os.makedirs(new_folder_path, exist_ok=True)
255
+ except Exception:
256
+ abort(500)
257
+
258
+ # Redirecionar de volta para o diretório atual
259
+ if current_path:
260
+ return redirect(url_for("browse", subpath=current_path))
261
+ else:
262
+ return redirect(url_for("browse"))
263
+
264
+
265
+ @app.route("/upload", methods=["POST"])
266
+ def upload_files():
267
+ current_path = request.form.get("path", "")
268
+
269
+ if not current_path:
270
+ abort(400)
271
+
272
+ # Extrair o nome da raiz
273
+ root_name = get_root_name_from_path(current_path)
274
+ if not root_name:
275
+ abort(404)
276
+
277
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
278
+ if not ROOT_DIR:
279
+ abort(404)
280
+
281
+ relative_path = remove_root_from_path(current_path)
282
+
283
+ # Validar diretório de destino
284
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
285
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
286
+ abort(403)
287
+
288
+ if not os.path.exists(full_path) or not os.path.isdir(full_path):
289
+ abort(404)
290
+
291
+ # Verificar se há arquivos no request
292
+ if "files" not in request.files:
293
+ abort(400)
294
+
295
+ files = request.files.getlist("files")
296
+
297
+ if not files:
298
+ abort(400)
299
+
300
+ # Salvar cada arquivo
301
+ for file in files:
302
+ if file and file.filename:
303
+ # Para uploads de pastas, preservar a estrutura de diretórios
304
+ filename = file.filename
305
+
306
+ # Normalizar o caminho (trocar barras e remover ../.. )
307
+ filename = filename.replace("\\", "/")
308
+
309
+ # Separar diretório e nome do arquivo
310
+ if "/" in filename:
311
+ # É um arquivo dentro de uma estrutura de pastas
312
+ parts = filename.split("/")
313
+ # Criar diretórios necessários
314
+ dir_path = os.path.join(full_path, *parts[:-1])
315
+ try:
316
+ os.makedirs(dir_path, exist_ok=True)
317
+ except Exception:
318
+ continue
319
+
320
+ # Nome do arquivo final
321
+ final_filename = secure_filename(parts[-1])
322
+ file_path = os.path.join(dir_path, final_filename)
323
+ else:
324
+ # Arquivo único
325
+ final_filename = secure_filename(filename)
326
+ file_path = os.path.join(full_path, final_filename)
327
+
328
+ try:
329
+ file.save(file_path)
330
+ except Exception:
331
+ # Continuar com os próximos arquivos mesmo se um falhar
332
+ continue
333
+
334
+ # Redirecionar de volta para o diretório atual
335
+ if current_path:
336
+ return redirect(url_for("browse", subpath=current_path))
337
+ else:
338
+ return redirect(url_for("browse"))
339
+
340
+
341
+ @app.route("/delete/<path:subpath>", methods=["POST"])
342
+ def delete_item(subpath):
343
+ import shutil
344
+
345
+ # Extrair o nome da raiz
346
+ root_name = get_root_name_from_path(subpath)
347
+ if not root_name:
348
+ abort(404)
349
+
350
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
351
+ if not ROOT_DIR:
352
+ abort(404)
353
+
354
+ relative_path = remove_root_from_path(subpath)
355
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
356
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
357
+ abort(403)
358
+
359
+ if not os.path.exists(full_path):
360
+ abort(404)
361
+
362
+ try:
363
+ if os.path.isdir(full_path):
364
+ shutil.rmtree(full_path)
365
+ else:
366
+ os.remove(full_path)
367
+ except Exception:
368
+ abort(500)
369
+
370
+ # Redirecionar de volta para o diretório pai
371
+ parent_path = "/".join(subpath.split("/")[:-1])
372
+ if parent_path:
373
+ return redirect(url_for("browse", subpath=parent_path))
374
+ else:
375
+ return redirect(url_for("browse"))
376
+
377
+
378
+ @app.route("/view/<path:subpath>")
379
+ def view_file(subpath):
380
+ # Extrair o nome da raiz
381
+ root_name = get_root_name_from_path(subpath)
382
+ if not root_name:
383
+ abort(404)
384
+
385
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
386
+ if not ROOT_DIR:
387
+ abort(404)
388
+
389
+ relative_path = remove_root_from_path(subpath)
390
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
391
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
392
+ abort(403)
393
+
394
+ if not os.path.exists(full_path) or os.path.isdir(full_path):
395
+ abort(404)
396
+
397
+ # Servir arquivo sem forçar download (inline)
398
+ return send_from_directory(os.path.dirname(full_path), os.path.basename(full_path))
399
+
400
+
401
+ @app.route("/download/<path:subpath>")
402
+ def download_file(subpath):
403
+ # Extrair o nome da raiz
404
+ root_name = get_root_name_from_path(subpath)
405
+ if not root_name:
406
+ abort(404)
407
+
408
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
409
+ if not ROOT_DIR:
410
+ abort(404)
411
+
412
+ relative_path = remove_root_from_path(subpath)
413
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
414
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
415
+ abort(403)
416
+
417
+ if not os.path.exists(full_path):
418
+ abort(404)
419
+
420
+ # Se for pasta, redirecionar para zipar
421
+ if os.path.isdir(full_path):
422
+ return redirect(url_for("download_zip", subpath=subpath))
423
+
424
+ # Forçar download do arquivo
425
+ return send_from_directory(
426
+ os.path.dirname(full_path), os.path.basename(full_path), as_attachment=True
427
+ )
428
+
429
+
430
+ @app.route("/api/search")
431
+ def api_search():
432
+ from flask import jsonify
433
+
434
+ query = request.args.get("q", "").lower().strip()
435
+
436
+ if not query:
437
+ return jsonify({"results": []})
438
+
439
+ results = []
440
+ max_results = 100 # Limitar resultados
441
+
442
+ try:
443
+ # Buscar em todas as pastas raiz
444
+ for root_name, ROOT_DIR in ROOT_DIRS:
445
+ if not os.path.exists(ROOT_DIR):
446
+ continue
447
+
448
+ for root, dirs, files in os.walk(ROOT_DIR):
449
+ # Verificar se ainda está dentro do ROOT_DIR
450
+ if not os.path.abspath(root).startswith(os.path.abspath(ROOT_DIR)):
451
+ continue
452
+
453
+ # Buscar em pastas
454
+ for dir_name in dirs:
455
+ if len(results) >= max_results:
456
+ break
457
+ if query in dir_name.lower():
458
+ full_path = os.path.join(root, dir_name)
459
+ rel_path = os.path.relpath(full_path, ROOT_DIR).replace(
460
+ "\\", "/"
461
+ )
462
+ parent_path = os.path.relpath(root, ROOT_DIR).replace("\\", "/")
463
+ if parent_path == ".":
464
+ parent_path = ""
465
+
466
+ # Adicionar o nome da raiz ao path
467
+ if parent_path:
468
+ full_rel_path = f"{root_name}/{rel_path}"
469
+ full_parent_path = f"{root_name}/{parent_path}"
470
+ else:
471
+ full_rel_path = f"{root_name}/{rel_path}"
472
+ full_parent_path = root_name
473
+
474
+ results.append(
475
+ {
476
+ "name": dir_name,
477
+ "path": full_rel_path,
478
+ "parent_path": full_parent_path,
479
+ "icon": get_file_icon(dir_name, True),
480
+ "is_dir": True,
481
+ }
482
+ )
483
+
484
+ # Buscar em arquivos
485
+ for file_name in files:
486
+ if len(results) >= max_results:
487
+ break
488
+ if query in file_name.lower():
489
+ full_path = os.path.join(root, file_name)
490
+ rel_path = os.path.relpath(full_path, ROOT_DIR).replace(
491
+ "\\", "/"
492
+ )
493
+ parent_path = os.path.relpath(root, ROOT_DIR).replace("\\", "/")
494
+ if parent_path == ".":
495
+ parent_path = ""
496
+
497
+ # Adicionar o nome da raiz ao path
498
+ if parent_path:
499
+ full_rel_path = f"{root_name}/{rel_path}"
500
+ full_parent_path = f"{root_name}/{parent_path}"
501
+ else:
502
+ full_rel_path = f"{root_name}/{rel_path}"
503
+ full_parent_path = root_name
504
+
505
+ results.append(
506
+ {
507
+ "name": file_name,
508
+ "path": full_rel_path,
509
+ "parent_path": full_parent_path,
510
+ "icon": get_file_icon(file_name, False),
511
+ "is_dir": False,
512
+ }
513
+ )
514
+
515
+ if len(results) >= max_results:
516
+ break
517
+
518
+ if len(results) >= max_results:
519
+ break
520
+
521
+ except Exception as e:
522
+ return jsonify({"results": [], "error": str(e)})
523
+
524
+ return jsonify({"results": results})
525
+
526
+
527
+ @app.route("/download_selected", methods=["POST"])
528
+ def download_selected():
529
+
530
+ selected_items = request.form.getlist("items[]")
531
+
532
+ if not selected_items:
533
+ abort(400)
534
+
535
+ # Se apenas 1 item e é arquivo, baixar direto
536
+ if len(selected_items) == 1:
537
+ subpath = selected_items[0]
538
+
539
+ # Extrair o nome da raiz
540
+ root_name = get_root_name_from_path(subpath)
541
+ if not root_name:
542
+ abort(404)
543
+
544
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
545
+ if not ROOT_DIR:
546
+ abort(404)
547
+
548
+ relative_path = remove_root_from_path(subpath)
549
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
550
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
551
+ abort(403)
552
+
553
+ if os.path.isfile(full_path):
554
+ return send_from_directory(
555
+ os.path.dirname(full_path),
556
+ os.path.basename(full_path),
557
+ as_attachment=True,
558
+ )
559
+
560
+ # Múltiplos items ou pasta: zipar
561
+ z = zipstream.ZipFile(mode="w", compression=zipstream.ZIP_DEFLATED)
562
+
563
+ # Determinar nome do arquivo ZIP
564
+ if len(selected_items) == 1:
565
+ zip_filename = os.path.basename(selected_items[0]) + ".zip"
566
+ else:
567
+ zip_filename = "download.zip"
568
+
569
+ for subpath in selected_items:
570
+ # Extrair o nome da raiz
571
+ root_name = get_root_name_from_path(subpath)
572
+ if not root_name:
573
+ continue
574
+
575
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
576
+ if not ROOT_DIR:
577
+ continue
578
+
579
+ relative_path = remove_root_from_path(subpath)
580
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
581
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
582
+ continue
583
+
584
+ if not os.path.exists(full_path):
585
+ continue
586
+
587
+ if os.path.isfile(full_path):
588
+ arcname = os.path.basename(full_path)
589
+ z.write(full_path, arcname)
590
+ elif os.path.isdir(full_path):
591
+ for root, dirs, files in os.walk(full_path):
592
+ for file in files:
593
+ file_path = os.path.join(root, file)
594
+ arcname = os.path.join(
595
+ os.path.basename(full_path),
596
+ os.path.relpath(file_path, full_path),
597
+ )
598
+ z.write(file_path, arcname)
599
+
600
+ response = Response(z, mimetype="application/zip")
601
+ response.headers["Content-Disposition"] = f"attachment; filename={zip_filename}"
602
+ return response
603
+
604
+
605
+ @app.route("/zip/<path:subpath>")
606
+ def download_zip(subpath):
607
+ # Extrair o nome da raiz
608
+ root_name = get_root_name_from_path(subpath)
609
+ if not root_name:
610
+ abort(404)
611
+
612
+ ROOT_DIR = get_root_dir(root_name, ROOT_DIRS)
613
+ if not ROOT_DIR:
614
+ abort(404)
615
+
616
+ relative_path = remove_root_from_path(subpath)
617
+ full_path = os.path.normpath(os.path.join(ROOT_DIR, relative_path))
618
+ if not full_path.startswith(os.path.abspath(ROOT_DIR)):
619
+ abort(403)
620
+
621
+ if not os.path.isdir(full_path):
622
+ abort(400)
623
+
624
+ # Criando o Stream de ZIP
625
+ z = zipstream.ZipFile(mode="w", compression=zipstream.ZIP_DEFLATED)
626
+
627
+ for root, dirs, files in os.walk(full_path):
628
+ for file in files:
629
+ file_path = os.path.join(root, file)
630
+ arcname = os.path.relpath(file_path, full_path)
631
+ z.write(file_path, arcname)
632
+
633
+ filename = os.path.basename(full_path) or "download"
634
+ response = Response(z, mimetype="application/zip")
635
+ response.headers["Content-Disposition"] = f"attachment; filename={filename}.zip"
636
+ return response