Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,36 +1,125 @@
|
|
1 |
import os
|
2 |
import subprocess
|
3 |
-
|
|
|
|
|
|
|
|
|
4 |
|
5 |
app = Flask(__name__)
|
6 |
|
7 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
@app.route("/generate-apk", methods=["POST"])
|
9 |
def generate_apk():
|
10 |
-
|
|
|
|
|
|
|
|
|
11 |
|
12 |
-
if not pwa_url:
|
13 |
-
return "URL
|
14 |
|
15 |
-
# Create
|
|
|
|
|
|
|
16 |
try:
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
except subprocess.CalledProcessError as e:
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import subprocess
|
3 |
+
import tempfile
|
4 |
+
import shutil
|
5 |
+
import re
|
6 |
+
from flask import Flask, request, send_file, jsonify
|
7 |
+
from werkzeug.utils import secure_filename
|
8 |
|
9 |
app = Flask(__name__)
|
10 |
|
11 |
+
# Configuration
|
12 |
+
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1MB max upload size
|
13 |
+
app.config['ALLOWED_HOSTS'] = ['yourdomain.com'] # Add your production domain
|
14 |
+
app.config['BUBBLEWRAP_MIN_VERSION'] = '1.0.0' # Minimum required version
|
15 |
+
|
16 |
+
# Validate URL format
|
17 |
+
def is_valid_url(url):
|
18 |
+
regex = re.compile(
|
19 |
+
r'^(https?://)?' # http:// or https://
|
20 |
+
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}' # domain
|
21 |
+
r'(/[a-zA-Z0-9-._~:/?#[\]@!$&\'()*+,;=]*)?$', # path
|
22 |
+
re.IGNORECASE
|
23 |
+
)
|
24 |
+
return re.match(regex, url) is not None
|
25 |
+
|
26 |
+
# Check if bubblewrap is available
|
27 |
+
def check_bubblewrap():
|
28 |
+
try:
|
29 |
+
result = subprocess.run(['bubblewrap', '--version'],
|
30 |
+
capture_output=True, text=True, check=True)
|
31 |
+
version = result.stdout.strip()
|
32 |
+
if version < app.config['BUBBLEWRAP_MIN_VERSION']:
|
33 |
+
raise Exception(f"Bubblewrap version {version} is below minimum required version")
|
34 |
+
return True
|
35 |
+
except Exception as e:
|
36 |
+
app.logger.error(f"Bubblewrap check failed: {str(e)}")
|
37 |
+
return False
|
38 |
+
|
39 |
+
# Clean up directory
|
40 |
+
def cleanup_directory(dir_path):
|
41 |
+
try:
|
42 |
+
if os.path.exists(dir_path):
|
43 |
+
shutil.rmtree(dir_path)
|
44 |
+
except Exception as e:
|
45 |
+
app.logger.error(f"Cleanup failed for {dir_path}: {str(e)}")
|
46 |
+
|
47 |
@app.route("/generate-apk", methods=["POST"])
|
48 |
def generate_apk():
|
49 |
+
# Validate request
|
50 |
+
if 'url' not in request.form:
|
51 |
+
return jsonify({"error": "URL parameter is required"}), 400
|
52 |
+
|
53 |
+
pwa_url = request.form['url'].strip()
|
54 |
|
55 |
+
if not is_valid_url(pwa_url):
|
56 |
+
return jsonify({"error": "Invalid URL format"}), 400
|
57 |
|
58 |
+
# Create temp directory
|
59 |
+
temp_dir = tempfile.mkdtemp(prefix='pwa2apk_')
|
60 |
+
os.chdir(temp_dir) # Work in temp directory
|
61 |
+
|
62 |
try:
|
63 |
+
# Initialize project
|
64 |
+
init_cmd = [
|
65 |
+
'bubblewrap', 'init',
|
66 |
+
'--manifest', pwa_url,
|
67 |
+
'--directory', temp_dir,
|
68 |
+
'--non-interactive'
|
69 |
+
]
|
70 |
|
71 |
+
subprocess.run(init_cmd, check=True, timeout=300,
|
72 |
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
73 |
+
|
74 |
+
# Build APK
|
75 |
+
build_cmd = ['bubblewrap', 'build']
|
76 |
+
subprocess.run(build_cmd, check=True, timeout=600,
|
77 |
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
78 |
+
|
79 |
+
# Verify APK was created
|
80 |
+
apk_path = os.path.join(temp_dir, 'app-release.apk')
|
81 |
+
if not os.path.exists(apk_path):
|
82 |
+
raise Exception("APK file not found after build")
|
83 |
+
|
84 |
+
# Secure filename for download
|
85 |
+
domain = pwa_url.split('//')[-1].split('/')[0].replace('.', '_')
|
86 |
+
apk_name = secure_filename(f"{domain}_app.apk")
|
87 |
+
|
88 |
+
# Return the file
|
89 |
+
return send_file(
|
90 |
+
apk_path,
|
91 |
+
as_attachment=True,
|
92 |
+
download_name=apk_name,
|
93 |
+
mimetype='application/vnd.android.package-archive'
|
94 |
+
)
|
95 |
+
|
96 |
+
except subprocess.TimeoutExpired:
|
97 |
+
return jsonify({"error": "APK generation timed out"}), 504
|
98 |
except subprocess.CalledProcessError as e:
|
99 |
+
app.logger.error(f"Build failed: {e.stderr}")
|
100 |
+
return jsonify({"error": "APK generation failed", "details": str(e)}), 500
|
101 |
+
except Exception as e:
|
102 |
+
app.logger.error(f"Unexpected error: {str(e)}")
|
103 |
+
return jsonify({"error": "APK generation failed", "details": str(e)}), 500
|
104 |
+
finally:
|
105 |
+
# Clean up
|
106 |
+
os.chdir('/') # Change out of directory before cleanup
|
107 |
+
cleanup_directory(temp_dir)
|
108 |
|
109 |
+
@app.before_request
|
110 |
+
def before_request():
|
111 |
+
# Simple host validation
|
112 |
+
if request.headers.get('Host') not in app.config['ALLOWED_HOSTS']:
|
113 |
+
return jsonify({"error": "Unauthorized host"}), 403
|
114 |
+
|
115 |
+
# Check bubblewrap is available
|
116 |
+
if not check_bubblewrap():
|
117 |
+
return jsonify({"error": "Service temporarily unavailable"}), 503
|
118 |
|
119 |
+
if __name__ == "__main__":
|
120 |
+
# Verify dependencies at startup
|
121 |
+
if not check_bubblewrap():
|
122 |
+
print("Error: Bubblewrap not available or version too old")
|
123 |
+
exit(1)
|
124 |
+
|
125 |
+
app.run(host="0.0.0.0", port=7860, threaded=True)
|