from models import db, File, User, UserPath, UploadTask from sqlalchemy import and_, or_, not_ import logging import os from datetime import datetime # --- 基礎查詢函數 --- def get_all_files(sort_by='date', sort_order='desc'): query = File.query.filter(File.filename != '.folder_marker', File.is_deleted == False) if sort_by == 'name': query = query.order_by(File.filename.asc() if sort_order == 'asc' else File.filename.desc()) elif sort_by == 'size': query = query.order_by(File.size.asc() if sort_order == 'asc' else File.size.desc()) elif sort_by == 'type': query = query.order_by(File.mime_type.asc() if sort_order == 'asc' else File.mime_type.desc()) else: query = query.order_by(File.upload_date.asc() if sort_order == 'asc' else File.upload_date.desc()) return query.all() def is_user_allowed_path(user_id, folder_path): user = User.query.get(user_id) if not user or user.username == 'admin': return True user_paths = [up.path for up in user.paths] if not user_paths: return False folder_path = folder_path.strip('/') for up in user_paths: up = up.strip('/') if folder_path == up or folder_path.startswith(up + '/'): return True return False def get_all_files_for_user(user_id, sort_by='date', sort_order='desc'): user = User.query.get(user_id) if not user: return [] if user.username == 'admin': return get_all_files(sort_by, sort_order) user_paths = [up.path for up in user.paths] if not user_paths: return [] query = File.query.filter(File.filename != '.folder_marker', File.is_deleted == False) path_filters = [File.folder.like(p + '%') for p in user_paths] query = query.filter(or_(*path_filters)) if sort_by == 'name': query = query.order_by(File.filename.asc() if sort_order == 'asc' else File.filename.desc()) elif sort_by == 'size': query = query.order_by(File.size.asc() if sort_order == 'asc' else File.size.desc()) elif sort_by == 'type': query = query.order_by(File.mime_type.asc() if sort_order == 'asc' else File.mime_type.desc()) else: query = query.order_by(File.upload_date.asc() if sort_order == 'asc' else File.upload_date.desc()) return query.all() def get_folder_contents_for_user(user_id, folder_path): folder_path = folder_path.strip('/') if not folder_path: folder_path = 'root' # 檢查是否有權限訪問該路徑 if not is_user_allowed_path(user_id, folder_path): # 即使沒有直接權限,如果是父級路徑,也允許瀏覽其子目錄(只要子目錄在允許列表裡) user = User.query.get(user_id) if not user: return None, None user_paths = [up.path.strip('/') for up in user.paths] # 檢查該 folder_path 是否是任何允許路徑的父目錄 is_parent_of_allowed = any(up.startswith(folder_path + '/') for up in user_paths) if not is_parent_of_allowed and folder_path != 'root': return None, None files = File.query.filter_by(folder=folder_path, is_deleted=False).filter(File.filename != '.folder_marker').all() subfolders_query = File.query.filter(File.filename == '.folder_marker', File.is_deleted == False) if folder_path == 'root': subfolders = subfolders_query.filter(or_(and_(not_(File.folder.contains('/')), File.folder != 'root'), File.folder.like('root/%'))).all() direct_subfolders = [] for sf in subfolders: rel = sf.folder[5:] if sf.folder.startswith('root/') else sf.folder if '/' not in rel and rel != '': direct_subfolders.append(sf) else: prefix = folder_path + "/" subfolders = subfolders_query.filter(File.folder.like(prefix + '%')).all() direct_subfolders = [sf for sf in subfolders if '/' not in sf.folder[len(prefix):]] # 再次過濾 subfolders 和 files user = User.query.get(user_id) if user and user.username != 'admin': user_paths = [up.path.strip('/') for up in user.paths] # 只保留有權限的文件 filtered_files = [f for f in files if any(f.folder == up or f.folder.startswith(up + '/') for up in user_paths)] # 資料夾過濾:如果是允許路徑,或者是允許路徑的父目錄 filtered_subfolders = [] for sf in direct_subfolders: sf_path = sf.folder.strip('/') is_allowed = any(sf_path == up or sf_path.startswith(up + '/') for up in user_paths) is_parent = any(up.startswith(sf_path + '/') for up in user_paths) if is_allowed or is_parent: filtered_subfolders.append(sf) return filtered_files, filtered_subfolders return files, direct_subfolders def search_files(query, path, file_type, min_size=None, max_size=None, start_date=None, end_date=None, sort_by='date', sort_order='desc', user_id=None): q = File.query.filter(File.is_deleted == False) # 用戶權限過濾 if user_id: user = User.query.get(user_id) if user and user.username != 'admin': user_paths = [up.path.strip('/') for up in user.paths] if not user_paths: return [] path_filters = [File.folder.like(p + '%') for p in user_paths] q = q.filter(or_(*path_filters)) if path and path != 'all' and path != 'root': q = q.filter(File.folder.like(f'{path}%')) if file_type and file_type != 'all': if file_type == 'folder': q = q.filter(File.filename == '.folder_marker') if query: q = q.filter(File.folder.like(f'%{query}%')) else: q = q.filter(File.mime_type.like(f'{file_type}%'), File.filename != '.folder_marker') if query: q = q.filter(File.filename.like(f'%{query}%')) else: if query: q = q.filter(or_( and_(File.filename != '.folder_marker', File.filename.like(f'%{query}%')), and_(File.filename == '.folder_marker', File.folder.like(f'%{query}%')) )) else: q = q.filter(File.filename != '.folder_marker') if min_size: q = q.filter(File.size >= min_size) if max_size: q = q.filter(File.size <= max_size) if start_date: q = q.filter(File.upload_date >= start_date) if end_date: q = q.filter(File.upload_date <= end_date) if sort_by == 'name': q = q.order_by(File.filename.asc() if sort_order == 'asc' else File.filename.desc()) elif sort_by == 'size': q = q.order_by(File.size.asc() if sort_order == 'asc' else File.size.desc()) elif sort_by == 'date': q = q.order_by(File.upload_date.asc() if sort_order == 'asc' else File.upload_date.desc()) else: q = q.order_by(File.upload_date.desc()) return q.all() def get_deleted_items(sort_by='date', sort_order='desc', user_id=None): query = File.query.filter_by(is_deleted=True) # 用戶權限過濾 if user_id: user = User.query.get(user_id) if user and user.username != 'admin': user_paths = [up.path.strip('/') for up in user.paths] if not user_paths: return [] path_filters = [File.folder.like(p + '%') for p in user_paths] query = query.filter(or_(*path_filters)) if sort_by == 'name': query = query.order_by(File.filename.asc() if sort_order == 'asc' else File.filename.desc()) elif sort_by == 'size': query = query.order_by(File.size.asc() if sort_order == 'asc' else File.size.desc()) elif sort_by == 'date': query = query.order_by(File.upload_date.asc() if sort_order == 'asc' else File.upload_date.desc()) else: query = query.order_by(File.upload_date.desc()) return query.all() # --- 檔案與資料夾操作 --- def get_file_by_id(db_id): return File.query.get(db_id) def add_file(filename, file_id, folder, size=None, mime_type=None, thumbnail_file_id=None, message_link=None): folder = folder.strip('/') if not folder: folder = 'root' new_file = File( filename=filename, file_id=file_id, folder=folder, size=size, mime_type=mime_type, thumbnail_file_id=thumbnail_file_id, message_link=message_link ) db.session.add(new_file); db.session.commit() logging.info(f"[DB-FLOW] Indexed: {filename} in {folder}") def _ensure_folder_path_exists(folder_path): folder_path = folder_path.strip('/') if not folder_path or folder_path == 'root': return parts = folder_path.split('/') current_path = "" for part in parts: current_path = (current_path + "/" + part) if current_path else part existing = File.query.filter_by(folder=current_path, filename='.folder_marker').first() if not existing: db.session.add(File(filename=".folder_marker", file_id="FOLDER_MARKER", folder=current_path, mime_type="application/x-folder")) db.session.commit() def rename_item(item_id, new_name, is_folder=False): item = File.query.get(item_id) if not item: return if is_folder: old_full_path = item.folder parent_path = os.path.dirname(old_full_path) new_full_path = os.path.join(parent_path, new_name).replace('\\', '/') files_to_update = File.query.filter(or_(File.folder == old_full_path, File.folder.like(old_full_path + '%'))).all() for f in files_to_update: f.folder = f.folder.replace(old_full_path, new_full_path, 1) else: item.filename = new_name db.session.commit() def restore_items(items): processed_ids = set() items_to_process = [int(item['id']) for item in items] while items_to_process: item_id = items_to_process.pop(0) if item_id in processed_ids: continue item = File.query.get(item_id) if not item: continue item.is_deleted = False processed_ids.add(item.id) if item.filename == '.folder_marker': folder_path = item.folder children = File.query.filter(or_(File.folder == folder_path, File.folder.like(f'{folder_path}/%'))).filter(File.is_deleted == True).all() for child in children: if child.id not in processed_ids: items_to_process.append(child.id) parent_path = item.folder if parent_path != 'root': parts = parent_path.split('/') current_path = '' for part in parts: current_path = f"{current_path}/{part}" if current_path else part parent_marker = File.query.filter_by(folder=current_path, filename='.folder_marker').first() if parent_marker and parent_marker.is_deleted: parent_marker.is_deleted = False; processed_ids.add(parent_marker.id) db.session.commit() def permanent_delete_items(items): for item in items: file = File.query.get(int(item['id'])) if file: db.session.delete(file) db.session.commit() def delete_item(item_ids, is_bulk=False): if not isinstance(item_ids, list): item_ids = [item_ids] for item_id in item_ids: item = File.query.get(item_id) if item: if item.filename == '.folder_marker': folder_path = item.folder File.query.filter(or_(File.folder == folder_path, File.folder.like(f'{folder_path}/%'))).update({File.is_deleted: True}, synchronize_session=False) else: item.is_deleted = True db.session.commit() def create_folder(folder_path): folder_path = folder_path.strip('/') if not folder_path: return root_marker = File.query.filter_by(folder='root', filename='.folder_marker').first() if not root_marker: db.session.add(File(filename=".folder_marker", file_id="FOLDER_MARKER", folder='root', mime_type="application/x-folder")) if folder_path == 'root': db.session.commit(); return path_parts = folder_path.split('/') current_path = '' for part in path_parts: current_path = f"{current_path}/{part}" if current_path else part existing = File.query.filter_by(folder=current_path, filename='.folder_marker').first() if not existing: db.session.add(File(filename=".folder_marker", file_id="FOLDER_MARKER", folder=current_path, mime_type="application/x-folder")) db.session.commit() def move_items(items, destination_folder): destination_folder = destination_folder.strip('/') if not destination_folder: destination_folder = 'root' # 強制修正 root/ 前綴,確保移動後的路徑可追蹤 if not destination_folder.startswith('root') and destination_folder != 'root': destination_folder = 'root/' + destination_folder if not File.query.filter_by(folder=destination_folder, filename='.folder_marker').first() and destination_folder != 'root': _ensure_folder_path_exists(destination_folder) for item_data in items: item = File.query.get(item_data['id']) if not item: continue if item_data['type'] == 'file': item.folder = destination_folder elif item_data['type'] == 'folder': old_folder_path = item.folder folder_name = os.path.basename(old_folder_path) new_folder_path = os.path.join(destination_folder, folder_name).replace('\\', '/') files_to_move = File.query.filter(File.folder.like(old_folder_path + '%')).all() for f in files_to_move: f.folder = f.folder.replace(old_folder_path, new_folder_path, 1) db.session.commit() def copy_items(items, destination_folder): destination_folder = destination_folder.strip('/') if not destination_folder: destination_folder = 'root' # 強制修正 root/ 前綴 if not destination_folder.startswith('root') and destination_folder != 'root': destination_folder = 'root/' + destination_folder if not File.query.filter_by(folder=destination_folder, filename='.folder_marker').first() and destination_folder != 'root': _ensure_folder_path_exists(destination_folder) for item_data in items: item = File.query.get(item_data['id']) if not item: continue if item_data['type'] == 'file': db.session.add(File(filename=item.filename, file_id=item.file_id, folder=destination_folder, size=item.size, mime_type=item.mime_type, thumbnail_file_id=item.thumbnail_file_id, message_link=item.message_link)) elif item_data['type'] == 'folder': old_folder_path = item.folder folder_name = os.path.basename(old_folder_path) new_folder_path = os.path.join(destination_folder, folder_name).replace('\\', '/') items_to_copy = File.query.filter(File.folder.like(old_folder_path + '%')).all() for itc in items_to_copy: new_item_folder = itc.folder.replace(old_folder_path, new_folder_path, 1) db.session.add(File(filename=itc.filename, file_id=itc.file_id, folder=new_item_folder, size=itc.size, mime_type=itc.mime_type, thumbnail_file_id=itc.thumbnail_file_id, message_link=itc.message_link)) db.session.commit() def bulk_rename_items(items, rename_method, new_name=None, rename_template=None, find_string=None, replace_string=None): for i, item_data in enumerate(items): item = File.query.get(item_data['id']) if not item: continue original_name = os.path.basename(item.folder) if item_data['type'] == 'folder' else item.filename name, ext = os.path.splitext(original_name) final_name = "" if rename_method == 'new_name': # 確保補上副檔名(如果是文件) if item_data['type'] == 'file' and not new_name.endswith(ext): final_name = new_name + ext else: final_name = new_name elif rename_method == 'template': # 支援多種佔位符,並確保保留 ext final_name = rename_template.replace('{i}', str(i+1)).replace('{name}', name).replace('{ext}', ext).replace('[original_name]', name).replace('[extension]', ext).replace('[counter]', str(i+1)) if item_data['type'] == 'file' and not final_name.endswith(ext): final_name += ext elif rename_method == 'find_replace': final_name = original_name.replace(find_string, replace_string) else: continue if not final_name or final_name == ext: continue # 防止空白名稱 if item_data['type'] == 'folder': old_full_path = item.folder parent_path = os.path.dirname(old_full_path) new_full_path = os.path.join(parent_path, final_name).replace('\\', '/') files_to_update = File.query.filter(File.folder.like(old_full_path + '%')).all() for f in files_to_update: f.folder = f.folder.replace(old_full_path, new_full_path, 1) else: item.filename = final_name db.session.commit() def get_subfolders_by_path(parent_path): """資料夾樹專用:獲取直接子資料夾標記""" parent_path = parent_path.strip('/') if not parent_path: parent_path = 'root' like_path = parent_path + "/%" all_markers = File.query.filter(File.folder.like(like_path), File.filename == '.folder_marker', File.is_deleted == False).all() return [m for m in all_markers if os.path.dirname(m.folder) == parent_path] def get_db_id_by_file_id(file_id): file = File.query.filter_by(file_id=file_id, is_deleted=False).first() return file.id if file else None def get_filename_by_telegram_file_id(telegram_file_id, include_deleted=False): file = get_file_by_telegram_file_id(telegram_file_id, include_deleted) return file.filename if file else None def get_file_by_telegram_file_id(telegram_file_id, include_deleted=False): query = File.query.filter_by(file_id=telegram_file_id) if not include_deleted: query = query.filter_by(is_deleted=False) return query.first() def get_file_by_telegram_file_id_including_deleted(telegram_file_id): return File.query.filter_by(file_id=telegram_file_id).first() def is_file_deleted(telegram_file_id): file = get_file_by_telegram_file_id(telegram_file_id, include_deleted=True) return file.is_deleted if file else False def get_root_folder_id(): file = File.query.filter_by(folder='root', filename='.folder_marker', is_deleted=False).first() return file.id if file else None def get_folder_contents(current_folder): files = File.query.filter_by(folder=current_folder, is_deleted=False).filter(File.filename != '.folder_marker').all() like_path = 'root/%' if current_folder == 'root' else current_folder + '/%' all_subfolders = File.query.filter(File.folder.like(like_path), File.filename == '.folder_marker', File.is_deleted == False).all() direct_subfolders = [sub for sub in all_subfolders if os.path.dirname(sub.folder) == current_folder] return files, direct_subfolders def empty_recycle_bin(): File.query.filter_by(is_deleted=True).delete(); db.session.commit() def get_upload_task_by_filename(filename): return UploadTask.query.filter_by(filename=filename).order_by(UploadTask.created_at.desc()).first() def get_upload_task_by_temp_path(temp_path): return UploadTask.query.filter_by(temp_file_path=temp_path).first() def bulk_delete_upload_tasks(task_ids): UploadTask.query.filter(UploadTask.id.in_(task_ids)).delete(synchronize_session=False); db.session.commit() def get_files_by_folder(folder): return File.query.filter_by(folder=folder, is_deleted=False).all() def get_all_folders(): return [r[0] for r in db.session.query(File.folder).filter(File.is_deleted == False).distinct()] # --- 上傳任務管理 --- def get_upload_task_by_id(task_id): return UploadTask.query.get(task_id) def add_upload_task_to_db(filename, temp_file_path, target_folder, file_size, mime_type, api_used, status='queued', priority=0, thumbnail_path=None): target_folder = target_folder.strip('/') if not target_folder: target_folder = 'root' new_task = UploadTask( filename=filename, temp_file_path=temp_file_path, target_folder=target_folder, file_size=file_size, mime_type=mime_type, api_used=api_used, status=status, priority=priority, thumbnail_path=thumbnail_path ) db.session.add(new_task); db.session.commit() logging.info(f"[DB-FLOW] Task {new_task.id} Queued: {filename}") def get_upload_tasks(status=None, limit=None, sort_by='created_at', sort_order='desc'): query = UploadTask.query if status: query = query.filter_by(status=status) # 動態排序 attr = getattr(UploadTask, sort_by, UploadTask.created_at) if sort_order == 'desc': query = query.order_by(attr.desc()) else: query = query.order_by(attr.asc()) if limit: query = query.limit(limit) return query.all() def update_upload_task_status(task_id, status, error_message=None): task = UploadTask.query.get(task_id) if task: task.status = status if error_message: task.error_message = error_message db.session.commit() def bulk_update_upload_task_status(task_ids, status, error_message=None): UploadTask.query.filter(UploadTask.id.in_(task_ids)).update({UploadTask.status: status, UploadTask.error_message: error_message}, synchronize_session=False) db.session.commit() def get_folder_id_by_path(path): path = path.strip('/') if not path: path = 'root' folder = File.query.filter_by(folder=path, filename='.folder_marker').first() return folder.id if folder else None def get_all_files_in_folder(folder_path): folder_path = folder_path.strip('/') return File.query.filter_by(folder=folder_path, is_deleted=False).all() # --- 用戶與管理 --- def add_user(username, password): if User.query.filter_by(username=username).first(): return None new_user = User(username=username, password=password) db.session.add(new_user); db.session.commit(); return new_user def get_user_by_id(user_id): return User.query.get(user_id) def get_user_by_username(username): return User.query.filter_by(username=username).first() def get_all_users(): return User.query.all() def delete_user(user_id): user = User.query.get(user_id); if user: db.session.delete(user); db.session.commit() def update_user_paths(user_id, paths): UserPath.query.filter_by(user_id=user_id).delete() for p in paths: if p.strip(): db.session.add(UserPath(user_id=user_id, path=p.strip())) db.session.commit() def get_user_paths(user_id): return [p.path for p in UserPath.query.filter_by(user_id=user_id).all()] def clear_database(): File.query.delete(); UploadTask.query.delete(); db.session.commit() def update_upload_task_priority(task_id, priority): task = UploadTask.query.get(task_id) if task: task.priority = priority; db.session.commit() def delete_upload_task(task_id): task = UploadTask.query.get(task_id) if task: db.session.delete(task); db.session.commit()