1- from flask import Flask
2- app = Flask (__name__ )
1+ import os
2+ import uuid
3+ from flask import Flask , render_template , request , redirect , url_for , session , flash
4+ from flask_wtf .csrf import CSRFProtect
5+ from werkzeug .security import generate_password_hash , check_password_hash
6+ from pymongo import MongoClient
7+ from bson import ObjectId
8+ from datetime import datetime
9+ from functools import wraps
10+ from dotenv import load_dotenv
11+ from azure .storage .blob import BlobServiceClient
312
13+ load_dotenv ()
14+
15+ app = Flask (__name__ , template_folder = "../frontend" , static_folder = "../static" )
16+ app .secret_key = os .environ .get ("SECRET_KEY" , "dev-secret" )
17+ app .config ["MAX_CONTENT_LENGTH" ] = 10 * 1024 * 1024 # 10 MB
18+
19+ csrf = CSRFProtect (app )
20+
21+ # ── COSMOS DB / MONGODB ATLAS ──────────────────────────────
22+ client = MongoClient (os .environ .get ("COSMOS_URI" ))
23+ db = client ["Picsource_db" ]
24+ images_col = db ["image_metadata" ]
25+ users_col = db ["user" ]
26+
27+ # ── AZURE BLOB STORAGE (connection string) ─────────────────
28+ BLOB_CONTAINER = os .environ .get ("BLOB_CONTAINER" , "images" )
29+ AZURE_STORAGE_ACCOUNT = os .environ .get ("AZURE_STORAGE_ACCOUNT" )
30+
31+ conn_str = os .environ .get ("AZURE_STORAGE_CONNECTION_STRING" )
32+ blob_service = BlobServiceClient .from_connection_string (conn_str )
33+ container_client = blob_service .get_container_client (BLOB_CONTAINER )
34+
35+ ALLOWED_EXTENSIONS = {"png" , "jpg" , "jpeg" , "gif" , "webp" , "tiff" , "bmp" , "heic" }
36+
37+ def allowed_file (filename ):
38+ return "." in filename and filename .rsplit ("." , 1 )[1 ].lower () in ALLOWED_EXTENSIONS
39+
40+ def get_blob_url (filename ):
41+ return f"https://{ AZURE_STORAGE_ACCOUNT } .blob.core.windows.net/{ BLOB_CONTAINER } /{ filename } "
42+
43+ def login_required (f ):
44+ @wraps (f )
45+ def decorated (* args , ** kwargs ):
46+ if "username" not in session :
47+ flash ("Please log in first." , "warning" )
48+ return redirect (url_for ("login" ))
49+ return f (* args , ** kwargs )
50+ return decorated
51+
52+ # ── REGISTER ───────────────────────────────────────────────
53+ @app .route ("/register" , methods = ["GET" , "POST" ])
54+ def register ():
55+ if request .method == "POST" :
56+ username = request .form .get ("username" , "" ).strip ()
57+ password = request .form .get ("password" , "" ).strip ()
58+ if users_col .find_one ({"username" : username }):
59+ flash ("Username already taken." , "error" )
60+ return redirect (url_for ("register" ))
61+ users_col .insert_one ({
62+ "username" : username ,
63+ "password" : generate_password_hash (password ),
64+ "created_at" : datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
65+ })
66+ flash ("Account created! Please log in." , "success" )
67+ return redirect (url_for ("login" ))
68+ return render_template ("register.html" )
69+
70+ # ── LOGIN ──────────────────────────────────────────────────
71+ @app .route ("/login" , methods = ["GET" , "POST" ])
72+ def login ():
73+ if request .method == "POST" :
74+ username = request .form .get ("username" , "" ).strip ()
75+ password = request .form .get ("password" , "" ).strip ()
76+
77+ if username == os .environ .get ("ADMIN_USERNAME" ) and \
78+ password == os .environ .get ("ADMIN_PASSWORD" ):
79+ session ["username" ] = username
80+ session ["is_admin" ] = True
81+ flash ("Welcome, Admin!" , "success" )
82+ return redirect (url_for ("index" ))
83+
84+ user = users_col .find_one ({"username" : username })
85+ if not user or not check_password_hash (user ["password" ], password ):
86+ flash ("Invalid username or password." , "error" )
87+ return redirect (url_for ("login" ))
88+ session ["username" ] = user ["username" ]
89+ session ["is_admin" ] = False
90+ flash (f"Welcome, { user ['username' ]} !" , "success" )
91+ return redirect (url_for ("index" ))
92+ return render_template ("login.html" )
93+
94+ # ── LOGOUT ─────────────────────────────────────────────────
95+ @app .route ("/logout" )
96+ def logout ():
97+ session .clear ()
98+ flash ("Logged out." , "info" )
99+ return redirect (url_for ("login" ))
100+
101+ # ── GALLERY ────────────────────────────────────────────────
4102@app .route ("/" )
5103def index ():
6- return "PicSource is alive!"
104+ images = list (images_col .find ())
105+ return render_template ("index.html" , images = images )
106+
107+ # ── UPLOAD ─────────────────────────────────────────────────
108+ @app .route ("/upload-page" )
109+ @login_required
110+ def upload_page ():
111+ return render_template ("upload.html" )
112+
113+ @app .route ("/upload" , methods = ["POST" ])
114+ @login_required
115+ def handle_upload ():
116+ if "file" not in request .files :
117+ flash ("No file part." , "error" )
118+ return redirect (url_for ("upload_page" ))
119+ file = request .files ["file" ]
120+ if file .filename == "" :
121+ flash ("No file selected." , "error" )
122+ return redirect (url_for ("upload_page" ))
123+ if not allowed_file (file .filename ):
124+ flash ("Invalid file type." , "error" )
125+ return redirect (url_for ("upload_page" ))
126+
127+ ext = os .path .splitext (file .filename )[1 ].lower ()
128+ unique_name = f"{ uuid .uuid4 ()} { ext } "
129+
130+ blob_client = container_client .get_blob_client (unique_name )
131+ file_bytes = file .read ()
132+ blob_client .upload_blob (file_bytes , overwrite = True )
133+
134+ size_bytes = len (file_bytes )
135+ tags = [t .strip () for t in request .form .get ("tags" , "" ).split ("," ) if t .strip ()]
136+
137+ images_col .insert_one ({
138+ "filename" : unique_name ,
139+ "image_url" : get_blob_url (unique_name ),
140+ "title" : request .form .get ("title" , "" ).strip (),
141+ "description" : request .form .get ("description" , "" ).strip (),
142+ "category" : request .form .get ("category" , "" ).strip (),
143+ "type" : request .form .get ("type" , "" ).strip (),
144+ "format" : request .form .get ("format" , "" ).strip (),
145+ "resolution" : request .form .get ("resolution" , "" ).strip (),
146+ "photographer" : request .form .get ("photographer" , "" ).strip (),
147+ "camera" : request .form .get ("camera" , "" ).strip (),
148+ "location" : request .form .get ("location" , "" ).strip (),
149+ "tags" : tags ,
150+ "size_kb" : round (size_bytes / 1024 , 2 ),
151+ "size_mb" : round (size_bytes / (1024 * 1024 ), 2 ),
152+ "upload_time" : datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" ),
153+ "uploaded_by" : session .get ("username" , "unknown" )
154+ })
155+ flash ("Image uploaded!" , "success" )
156+ return redirect (url_for ("index" ))
157+
158+ # ── DELETE ─────────────────────────────────────────────────
159+ @app .route ("/delete/<filename>" , methods = ["POST" ])
160+ @login_required
161+ def delete_image (filename ):
162+ if not session .get ("is_admin" ):
163+ flash ("Only admin can delete images." , "error" )
164+ return redirect (url_for ("index" ))
165+ img = images_col .find_one ({"filename" : filename })
166+ if not img :
167+ flash ("Image not found." , "error" )
168+ return redirect (url_for ("index" ))
169+ try :
170+ container_client .get_blob_client (filename ).delete_blob ()
171+ except Exception :
172+ pass
173+ images_col .delete_one ({"filename" : filename })
174+ flash ("Image deleted." , "info" )
175+ return redirect (url_for ("index" ))
176+
177+ # ── UPDATE (metadata only) ─────────────────────────────────
178+ @app .route ("/update/<filename>" , methods = ["POST" ])
179+ @login_required
180+ def update_image (filename ):
181+ if not session .get ("is_admin" ):
182+ flash ("Only admin can edit images." , "error" )
183+ return redirect (url_for ("index" ))
184+ img = images_col .find_one ({"filename" : filename })
185+ if not img :
186+ flash ("Image not found." , "error" )
187+ return redirect (url_for ("index" ))
188+ tags = [t .strip () for t in request .form .get ("tags" , "" ).split ("," ) if t .strip ()]
189+ images_col .update_one ({"filename" : filename }, {"$set" : {
190+ "title" : request .form .get ("title" , "" ).strip (),
191+ "description" : request .form .get ("description" , "" ).strip (),
192+ "category" : request .form .get ("category" , "" ).strip (),
193+ "type" : request .form .get ("type" , "" ).strip (),
194+ "format" : request .form .get ("format" , "" ).strip (),
195+ "resolution" : request .form .get ("resolution" , "" ).strip (),
196+ "photographer" : request .form .get ("photographer" , "" ).strip (),
197+ "camera" : request .form .get ("camera" , "" ).strip (),
198+ "location" : request .form .get ("location" , "" ).strip (),
199+ "tags" : tags ,
200+ }})
201+ flash ("Image updated!" , "success" )
202+ return redirect (url_for ("index" ))
7203
8204if __name__ == "__main__" :
9- app .run ()
205+ debug = os .environ .get ("FLASK_DEBUG" , "false" ).lower () == "true"
206+ port = int (os .environ .get ("PORT" , 5001 ))
207+ app .run (debug = debug , port = port )
0 commit comments