webtools-cli 1.2.0__tar.gz → 1.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
@@ -29,6 +29,8 @@ Requires-Dist: Pillow
29
29
  Requires-Dist: mtranslate
30
30
  Requires-Dist: colorama
31
31
  Requires-Dist: playwright
32
+ Requires-Dist: mega.py
33
+ Requires-Dist: pycryptodome
32
34
  Requires-Dist: pyreadline3; platform_system == "Windows"
33
35
  Provides-Extra: playwright
34
36
  Requires-Dist: playwright; extra == "playwright"
@@ -52,6 +54,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
52
54
 
53
55
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
54
56
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
57
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
55
58
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
56
59
  - **💻 Terminal-First**: Designed for power users who live in the command line.
57
60
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -84,6 +87,8 @@ pip install webtools-cli --upgrade
84
87
  ### Advanced Scraping & Stealth
85
88
  - **Smart Proxy Rotation**: Automatically rotates User-Agents and Proxies to evade detection.
86
89
  - **Turbo-Fetch**: Parallel chunk downloads for large media (Videos/Images).
90
+ - **Mega Cloud Drive**: Native integration with Mega.nz for multi-account storage and file management.
91
+ - **One-Click Save**: Instantly save scraped images and videos directly to your cloud storage.
87
92
  - **Deep Crawl**: Recursive link mapping up to 3 levels deep.
88
93
  - **Headless Fallback**: Integrated Playwright support for auth-walled or SPA environments.
89
94
 
@@ -16,6 +16,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
16
16
 
17
17
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
18
18
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
19
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
19
20
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
20
21
  - **💻 Terminal-First**: Designed for power users who live in the command line.
21
22
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -48,6 +49,8 @@ pip install webtools-cli --upgrade
48
49
  ### Advanced Scraping & Stealth
49
50
  - **Smart Proxy Rotation**: Automatically rotates User-Agents and Proxies to evade detection.
50
51
  - **Turbo-Fetch**: Parallel chunk downloads for large media (Videos/Images).
52
+ - **Mega Cloud Drive**: Native integration with Mega.nz for multi-account storage and file management.
53
+ - **One-Click Save**: Instantly save scraped images and videos directly to your cloud storage.
51
54
  - **Deep Crawl**: Recursive link mapping up to 3 levels deep.
52
55
  - **Headless Fallback**: Integrated Playwright support for auth-walled or SPA environments.
53
56
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "webtools-cli"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -36,6 +36,8 @@ dependencies = [
36
36
  "mtranslate",
37
37
  "colorama",
38
38
  "playwright",
39
+ "mega.py",
40
+ "pycryptodome",
39
41
  "pyreadline3; platform_system == 'Windows'",
40
42
  ]
41
43
 
@@ -7,6 +7,32 @@ if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
7
7
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
8
8
  except Exception: pass
9
9
 
10
+ # --- AUTO-INSTALLER FOR MISSING DEPENDENCIES ---
11
+ def ensure_dependencies():
12
+ """Checks and installs missing dependencies at runtime"""
13
+ dependencies = {
14
+ "mega": "mega.py",
15
+ "Crypto": "pycryptodome"
16
+ }
17
+ missing = []
18
+ for module_name, package_name in dependencies.items():
19
+ try:
20
+ __import__(module_name)
21
+ except ImportError:
22
+ missing.append(package_name)
23
+
24
+ if missing:
25
+ print(f"\n[!] Missing dependencies detected: {', '.join(missing)}")
26
+ print("[*] Attempting automatic installation...")
27
+ try:
28
+ subprocess.check_call([sys.executable, "-m", "pip", "install"] + missing)
29
+ print("[+] Dependencies installed successfully!\n")
30
+ except Exception as e:
31
+ print(f"[-] Auto-installation failed: {e}")
32
+ print("[!] Please run: pip install mega.py pycryptodome")
33
+
34
+ ensure_dependencies()
35
+
10
36
 
11
37
  # --- PACKAGE PATHS ---
12
38
  PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -23,9 +49,179 @@ except ImportError:
23
49
  import numpy as np
24
50
  from bs4 import BeautifulSoup
25
51
  from collections import Counter
26
- from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file
52
+ from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file, after_this_request
27
53
  from PIL import Image,ExifTags,ImageChops,ImageEnhance
28
54
  from io import BytesIO
55
+ from mega import Mega
56
+ try:
57
+ from mega.errors import RequestError
58
+ from mega.crypto import (
59
+ base64_to_a32, decrypt_key, base64_url_decode, a32_to_str,
60
+ encrypt_key, str_to_a32, mpi_to_int, base64_url_encode
61
+ )
62
+ from Crypto.PublicKey import RSA
63
+ import binascii
64
+ except ImportError:
65
+ class RequestError(Exception): pass
66
+
67
+ # --- MONKEY PATCH FOR MEGA.PY (Fixes TypeError in AES calls) ---
68
+ try:
69
+ from Crypto.Cipher import AES
70
+ import mega.mega as mega_module
71
+ _original_aes_new = AES.new
72
+
73
+ def _patched_aes_new(key, *args, **kwargs):
74
+ # Convert key to bytes if string
75
+ if isinstance(key, str):
76
+ key = key.encode('latin-1')
77
+
78
+ # Convert positional arguments (like IV/mode) to bytes if strings
79
+ new_args = list(args)
80
+ for i in range(len(new_args)):
81
+ if isinstance(new_args[i], str):
82
+ new_args[i] = new_args[i].encode('latin-1')
83
+
84
+ # Convert keyword arguments (IV, nonce, etc.) to bytes if strings
85
+ for k in kwargs:
86
+ if isinstance(kwargs[k], str):
87
+ kwargs[k] = kwargs[k].encode('latin-1')
88
+
89
+ return _original_aes_new(key, *new_args, **kwargs)
90
+
91
+ # Apply patch globally
92
+ AES.new = _patched_aes_new
93
+ if hasattr(mega_module, 'AES'):
94
+ mega_module.AES.new = _patched_aes_new
95
+ except Exception:
96
+ pass
97
+
98
+ class RobustMega(Mega):
99
+ """
100
+ Patched version of Mega library to handle fragile API responses.
101
+ The original library crashes if the API returns non-JSON or an empty string.
102
+ """
103
+ def _login_process(self, resp, password):
104
+ """
105
+ Overridden to fix 'Invalid RSA public exponent' error in newer pycryptodome.
106
+ The original code passes 0 as the exponent, which is rejected.
107
+ """
108
+ encrypted_master_key = base64_to_a32(resp['k'])
109
+ self.master_key = decrypt_key(encrypted_master_key, password)
110
+ if 'tsid' in resp:
111
+ tsid = base64_url_decode(resp['tsid'])
112
+ key_encrypted = a32_to_str(
113
+ encrypt_key(str_to_a32(tsid[:16]), self.master_key)
114
+ )
115
+ if key_encrypted == tsid[-16:]:
116
+ self.sid = resp['tsid']
117
+ elif 'csid' in resp:
118
+ encrypted_rsa_private_key = base64_to_a32(resp['privk'])
119
+ rsa_private_key = decrypt_key(
120
+ encrypted_rsa_private_key, self.master_key
121
+ )
122
+
123
+ private_key_str = a32_to_str(rsa_private_key)
124
+ self.rsa_private_key = [0, 0, 0, 0]
125
+
126
+ # This loop parses the 4 components: p, q, d, u
127
+ for i in range(4):
128
+ l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
129
+ self.rsa_private_key[i] = mpi_to_int(private_key_str[:l])
130
+ private_key_str = private_key_str[l:]
131
+
132
+ encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
133
+
134
+ # The Fix: Calculate the real 'e' from p, q, d
135
+ p = self.rsa_private_key[0]
136
+ q = self.rsa_private_key[1]
137
+ d = self.rsa_private_key[2]
138
+ n = p * q
139
+ phi = (p - 1) * (q - 1)
140
+
141
+ try:
142
+ # Calculate real e = d^-1 mod phi
143
+ e = pow(d, -1, phi)
144
+ rsa_decrypter = RSA.construct((n, e, d, p, q))
145
+ except:
146
+ # Fallback to 65537 if inverse fails (shouldn't happen)
147
+ e = 65537
148
+ rsa_decrypter = RSA.construct((n, e, d, p, q), consistency_check=False)
149
+
150
+ # Handle potential library differences in how the key is accessed
151
+ key_obj = rsa_decrypter.key if hasattr(rsa_decrypter, 'key') else rsa_decrypter
152
+
153
+ # Decrypt sid integer
154
+ sid_int = key_obj._decrypt(encrypted_sid)
155
+
156
+ # Convert to hex then to bytes, ensuring we handle the 0 padding if any
157
+ sid_hex = '%x' % sid_int
158
+ if len(sid_hex) % 2:
159
+ sid_hex = '0' + sid_hex
160
+ sid_bytes = binascii.unhexlify(sid_hex)
161
+
162
+ self.sid = base64_url_encode(sid_bytes[:43])
163
+
164
+ def _api_request(self, data):
165
+ params = {'id': self.sequence_num}
166
+ self.sequence_num += 1
167
+
168
+ if self.sid:
169
+ params.update({'sid': self.sid})
170
+
171
+ if not isinstance(data, list):
172
+ data = [data]
173
+
174
+ url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
175
+ headers = {
176
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
177
+ 'Accept': '*/*',
178
+ 'Accept-Language': 'en-US,en;q=0.9',
179
+ 'Origin': 'https://mega.nz',
180
+ 'Referer': 'https://mega.nz/'
181
+ }
182
+
183
+ try:
184
+ req = requests.post(
185
+ url,
186
+ params=params,
187
+ data=json.dumps(data),
188
+ headers=headers,
189
+ timeout=self.timeout,
190
+ )
191
+
192
+ content = req.text.strip() if req.text else ""
193
+
194
+ if not content:
195
+ if req.status_code == 402:
196
+ raise Exception("Mega.nz is temporarily blocking this connection (Status 402). This usually happens due to rate-limiting. Please try again later or use a different network.")
197
+ raise Exception(f"Mega API returned empty response (Status {req.status_code})")
198
+
199
+ try:
200
+ json_resp = json.loads(content)
201
+ except json.JSONDecodeError:
202
+ # Try to parse as integer error code
203
+ try:
204
+ val = int(content)
205
+ raise RequestError(val)
206
+ except (ValueError, RequestError):
207
+ if content.startswith("-") and content[1:].isdigit():
208
+ raise RequestError(int(content))
209
+ raise Exception(f"Mega API error: {content[:100]}")
210
+
211
+ if isinstance(json_resp, int):
212
+ if json_resp == -3:
213
+ time.sleep(0.5)
214
+ return self._api_request(data=data)
215
+ raise RequestError(json_resp)
216
+
217
+ return json_resp[0]
218
+ except Exception:
219
+ raise
220
+
221
+ # Global Mega session state
222
+ mega_engine = RobustMega()
223
+ mega_sessions = {} # Mapping: email -> {'client': MegaClient, 'info': user_info, 'quota': quota}
224
+ active_mega_email = None
29
225
 
30
226
  try:
31
227
  from playwright.sync_api import sync_playwright
@@ -144,6 +340,8 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
144
340
  # Directories setup kar rahe hain
145
341
  os.makedirs(os.path.join(SCRAPED_DIR, 'images'), exist_ok=True)
146
342
  os.makedirs(os.path.join(SCRAPED_DIR, 'videos'), exist_ok=True)
343
+ CLOUD_DIR = os.path.join(os.getcwd(), 'cloud_storage')
344
+ os.makedirs(CLOUD_DIR, exist_ok=True)
147
345
 
148
346
  # --- PERFORMANCE AUDITOR ---
149
347
  class PerformanceTracker:
@@ -429,12 +627,339 @@ def serve_js():
429
627
  def serve_favicon():
430
628
  return send_from_directory(WEB_DIR, 'Web_Tools.png')
431
629
 
432
- @app.route('/download/<path:filename>')
433
-
434
630
  @app.route('/download/<path:filename>')
435
631
  def serve_scraped_file(filename):
436
632
  return send_from_directory(SCRAPED_DIR, filename)
437
633
 
634
+ # --- CLOUD STORAGE APIs (MEGA.NZ) ---
635
+
636
+ def _get_node_by_path(path, client=None):
637
+ if not client:
638
+ global active_mega_email, mega_sessions
639
+ if not active_mega_email or active_mega_email not in mega_sessions:
640
+ return None
641
+ client = mega_sessions[active_mega_email]['client']
642
+
643
+ if not path or path == '/':
644
+ files = client.get_files()
645
+ for k, v in files.items():
646
+ if v.get('t') == 2: # Root node
647
+ return k
648
+ return None
649
+ else:
650
+ node = client.find(path)
651
+ if node:
652
+ if isinstance(node, tuple):
653
+ return node[0]
654
+ elif hasattr(node, '__getitem__'):
655
+ return node[0]
656
+ return None
657
+
658
+ def _get_or_create_node(name, parent_id, client):
659
+ """Find a folder by name under parent_id, or create it if missing."""
660
+ files = client.get_files()
661
+ for k, v in files.items():
662
+ if v.get('p') == parent_id and v.get('t') == 1 and v.get('a', {}).get('n') == name:
663
+ return k
664
+ # Not found, create it
665
+ new_node = client.create_folder(name, dest=parent_id)
666
+ # create_folder returns the new dict or node id depending on version
667
+ if isinstance(new_node, dict):
668
+ # find it again to be sure
669
+ files = client.get_files()
670
+ for k, v in files.items():
671
+ if v.get('p') == parent_id and v.get('t') == 1 and v.get('a', {}).get('n') == name:
672
+ return k
673
+ return new_node
674
+
675
+ @app.route('/api/cloud/login', methods=['POST'])
676
+ def cloud_login():
677
+ global mega_sessions, active_mega_email
678
+ try:
679
+ data = request.json or {}
680
+ email = data.get('email', '').strip()
681
+ password = data.get('password', '').strip()
682
+ if not email or not password:
683
+ return jsonify({'success': False, 'error': 'Email and password required'}), 400
684
+
685
+ client = mega_engine.login(email, password)
686
+ user_info = client.get_user()
687
+ quota_raw = client.get_quota()
688
+ # Normalize quota (some versions use total/space_used, others total/used)
689
+ quota = {
690
+ 'total': quota_raw.get('total', 0) if isinstance(quota_raw, dict) else 0,
691
+ 'used': quota_raw.get('space_used', quota_raw.get('used', 0)) if isinstance(quota_raw, dict) else 0
692
+ }
693
+
694
+ # Store in sessions
695
+ mega_sessions[email] = {
696
+ 'client': client,
697
+ 'info': user_info,
698
+ 'quota': quota
699
+ }
700
+ active_mega_email = email
701
+
702
+ return jsonify({
703
+ 'success': True,
704
+ 'message': f'Logged in as {email}',
705
+ 'user': user_info,
706
+ 'quota': quota,
707
+ 'active_email': active_mega_email,
708
+ 'all_accounts': list(mega_sessions.keys())
709
+ })
710
+ except Exception as e:
711
+ error_msg = str(e)
712
+ if "-9" in error_msg: error_msg = "Invalid email or password"
713
+ elif "-11" in error_msg: error_msg = "Access denied (Blocked/Banned)"
714
+ elif "-3" in error_msg: error_msg = "Too many requests, please wait"
715
+ return jsonify({'success': False, 'error': error_msg}), 401
716
+
717
+ @app.route('/api/cloud/switch_account', methods=['POST'])
718
+ def cloud_switch_account():
719
+ global active_mega_email, mega_sessions
720
+ data = request.json or {}
721
+ email = data.get('email')
722
+ if email in mega_sessions:
723
+ active_mega_email = email
724
+ return jsonify({'success': True, 'active_email': active_mega_email})
725
+ return jsonify({'success': False, 'error': 'Account not found'}), 404
726
+
727
+ @app.route('/api/cloud/check_auth', methods=['GET'])
728
+ def cloud_check_auth():
729
+ global active_mega_email, mega_sessions
730
+ accounts = []
731
+ for email, session in mega_sessions.items():
732
+ try:
733
+ # Quick check if still valid
734
+ quota_raw = session['client'].get_quota()
735
+ quota = {
736
+ 'total': quota_raw.get('total', 0) if isinstance(quota_raw, dict) else 0,
737
+ 'used': quota_raw.get('space_used', quota_raw.get('used', 0)) if isinstance(quota_raw, dict) else 0
738
+ }
739
+ session['quota'] = quota # update quota
740
+ accounts.append({
741
+ 'email': email,
742
+ 'user': session['info'],
743
+ 'quota': quota
744
+ })
745
+ except:
746
+ # Session might have died
747
+ pass
748
+
749
+ return jsonify({
750
+ 'success': True,
751
+ 'authenticated': len(accounts) > 0,
752
+ 'active_email': active_mega_email,
753
+ 'accounts': accounts
754
+ })
755
+
756
+ @app.route('/api/cloud/logout', methods=['POST'])
757
+ def cloud_logout():
758
+ global mega_sessions, active_mega_email
759
+ data = request.json or {}
760
+ email = data.get('email', active_mega_email)
761
+ if email in mega_sessions:
762
+ del mega_sessions[email]
763
+ if active_mega_email == email:
764
+ active_mega_email = list(mega_sessions.keys())[0] if mega_sessions else None
765
+ return jsonify({'success': True, 'message': 'Logged out', 'active_email': active_mega_email})
766
+
767
+ @app.route('/api/cloud/files', methods=['GET'])
768
+ def cloud_list_files():
769
+ global mega_sessions, active_mega_email
770
+ email = request.args.get('email', active_mega_email)
771
+ if not email or email not in mega_sessions:
772
+ return jsonify({'success': False, 'error': 'Not authenticated'}), 401
773
+
774
+ try:
775
+ client = mega_sessions[email]['client']
776
+ req_path = request.args.get('path', '').strip('/')
777
+ parent_id = _get_node_by_path(req_path, client)
778
+ if not parent_id:
779
+ return jsonify({'success': False, 'error': 'Directory not found'}), 404
780
+
781
+ files = client.get_files()
782
+ items = []
783
+ for k, v in files.items():
784
+ if v.get('p') == parent_id:
785
+ items.append({
786
+ 'name': v.get('a', {}).get('n'),
787
+ 'is_dir': v.get('t') == 1,
788
+ 'size': v.get('s', 0),
789
+ 'modified_at': v.get('ts', 0)
790
+ })
791
+
792
+ items.sort(key=lambda x: (not x['is_dir'], (x['name'] or '').lower()))
793
+ return jsonify({'success': True, 'path': req_path, 'items': items})
794
+ except Exception as e:
795
+ return jsonify({'success': False, 'error': str(e)}), 500
796
+
797
+ @app.route('/api/cloud/import_local', methods=['POST'])
798
+ def cloud_import_local():
799
+ global mega_sessions, active_mega_email
800
+ try:
801
+ data = request.json or {}
802
+ local_path = data.get('path') # relative to SCRAPED_DIR or static/uploads?
803
+ file_type = data.get('type') # 'image' or 'video'
804
+ page_title = data.get('title', 'Imported Media').strip() or 'Imported Media'
805
+ email = data.get('email', active_mega_email)
806
+
807
+ if not email or email not in mega_sessions:
808
+ return jsonify({'success': False, 'error': 'Cloud login required'}), 401
809
+
810
+ if not local_path:
811
+ return jsonify({'success': False, 'error': 'No file path provided'}), 400
812
+
813
+ # Handle local path check
814
+ # Usually scraped files are in SCRAPED_DIR
815
+ # If path starts with /download/ we strip it
816
+ clean_path = local_path.split('?')[0] # remove cache busters
817
+ if clean_path.startswith('/download/'):
818
+ filename = clean_path.replace('/download/', '')
819
+ disk_path = os.path.join(SCRAPED_DIR, filename)
820
+ else:
821
+ # Ad-hoc filenames
822
+ filename = os.path.basename(clean_path)
823
+ disk_path = os.path.join(SCRAPED_DIR, filename)
824
+
825
+ if not os.path.exists(disk_path):
826
+ return jsonify({'success': False, 'error': f'File not found: {filename}'}), 404
827
+
828
+ client = mega_sessions[email]['client']
829
+
830
+ # 1. Ensure Root Folder for this Page
831
+ root_node = _get_node_by_path('', client)
832
+ page_node = _get_or_create_node(page_title, root_node, client)
833
+
834
+ # 2. Ensure Type Folder (Images / Videos)
835
+ type_folder_name = "Images" if file_type == 'image' else "Videos"
836
+ type_node = _get_or_create_node(type_folder_name, page_node, client)
837
+
838
+ # 3. Upload
839
+ client.upload(disk_path, dest=type_node)
840
+
841
+ return jsonify({'success': True, 'message': f'Saved to {page_title}/{type_folder_name}'})
842
+ except Exception as e:
843
+ return jsonify({'success': False, 'error': str(e)}), 500
844
+
845
+ @app.route('/api/cloud/upload', methods=['POST'])
846
+ def cloud_upload():
847
+ global mega_sessions, active_mega_email
848
+ email = request.form.get('email', active_mega_email)
849
+ if not email or email not in mega_sessions:
850
+ return jsonify({'success': False, 'error': 'Not authenticated'}), 401
851
+ try:
852
+ client = mega_sessions[email]['client']
853
+ req_path = request.form.get('path', '').strip('/')
854
+ parent_id = _get_node_by_path(req_path, client)
855
+ if not parent_id:
856
+ return jsonify({'success': False, 'error': 'Invalid path'}), 403
857
+
858
+ if 'file' not in request.files:
859
+ return jsonify({'success': False, 'error': 'No file part'}), 400
860
+
861
+ file = request.files['file']
862
+ if file.filename == '':
863
+ return jsonify({'success': False, 'error': 'No selected file'}), 400
864
+
865
+ from werkzeug.utils import secure_filename
866
+ filename = secure_filename(file.filename)
867
+
868
+ temp_dir = os.path.join(PACKAGE_DIR, 'tmp_mega')
869
+ os.makedirs(temp_dir, exist_ok=True)
870
+ temp_path = os.path.join(temp_dir, filename)
871
+ file.save(temp_path)
872
+
873
+ client.upload(temp_path, parent_id)
874
+
875
+ try: os.remove(temp_path)
876
+ except: pass
877
+
878
+ return jsonify({'success': True, 'message': 'File uploaded successfully'})
879
+ except Exception as e:
880
+ import traceback
881
+ traceback.print_exc()
882
+ return jsonify({'success': False, 'error': str(e)}), 500
883
+
884
+ @app.route('/api/cloud/mkdir', methods=['POST'])
885
+ def cloud_mkdir():
886
+ global mega_sessions, active_mega_email
887
+ data = request.json or {}
888
+ email = data.get('email', active_mega_email)
889
+ if not email or email not in mega_sessions:
890
+ return jsonify({'success': False, 'error': 'Not authenticated'}), 401
891
+ try:
892
+ client = mega_sessions[email]['client']
893
+ req_path = data.get('path', '').strip('/')
894
+ folder_name = data.get('folder_name', '').strip()
895
+
896
+ if not folder_name:
897
+ return jsonify({'success': False, 'error': 'Folder name required'}), 400
898
+
899
+ parent_id = _get_node_by_path(req_path, client)
900
+ if not parent_id:
901
+ return jsonify({'success': False, 'error': 'Invalid path'}), 403
902
+
903
+ client.create_folder(folder_name, parent_id)
904
+ return jsonify({'success': True, 'message': 'Folder created'})
905
+ except Exception as e:
906
+ import traceback
907
+ traceback.print_exc()
908
+ return jsonify({'success': False, 'error': str(e)}), 500
909
+
910
+ @app.route('/api/cloud/delete', methods=['DELETE'])
911
+ def cloud_delete():
912
+ global mega_sessions, active_mega_email
913
+ data = request.json or {}
914
+ email = data.get('email', active_mega_email)
915
+ if not email or email not in mega_sessions:
916
+ return jsonify({'success': False, 'error': 'Not authenticated'}), 401
917
+ try:
918
+ client = mega_sessions[email]['client']
919
+ req_path = data.get('path', '').strip('/')
920
+ item_name = data.get('item_name', '').strip()
921
+
922
+ target_path = f"{req_path}/{item_name}".strip('/')
923
+ node_id = _get_node_by_path(target_path, client)
924
+
925
+ if not node_id:
926
+ return jsonify({'success': False, 'error': 'Item not found'}), 404
927
+
928
+ client.destroy(node_id)
929
+ return jsonify({'success': True, 'message': 'Item deleted'})
930
+ except Exception as e:
931
+ return jsonify({'success': False, 'error': str(e)}), 500
932
+
933
+ @app.route('/api/cloud/download/<path:filepath>')
934
+ def cloud_download(filepath):
935
+ global mega_sessions, active_mega_email
936
+ email = request.args.get('email', active_mega_email)
937
+ if not email or email not in mega_sessions:
938
+ return "Not authenticated", 401
939
+ try:
940
+ client = mega_sessions[email]['client']
941
+ node_id = _get_node_by_path(filepath, client)
942
+ if not node_id:
943
+ return "File not found", 404
944
+
945
+ # Download to local temp folder, then serve it
946
+ temp_dir = os.path.join(PACKAGE_DIR, 'tmp_mega_dl')
947
+ os.makedirs(temp_dir, exist_ok=True)
948
+ # find node object first
949
+ files = client.get_files()
950
+ node = {node_id: files[node_id]}
951
+ downloaded_path = client.download(node, temp_dir)
952
+
953
+ @after_this_request
954
+ def remove_file(response):
955
+ try: os.remove(downloaded_path)
956
+ except: pass
957
+ return response
958
+
959
+ return send_file(downloaded_path, as_attachment=True)
960
+ except Exception as e:
961
+ return str(e), 500
962
+
438
963
  def _run_playwright_scrape(url):
439
964
  """Internal: Actually run the Playwright scrape"""
440
965
  with sync_playwright() as p: