VvvebJs / storage.php
CatPtain's picture
Upload 5 files
92a44b9 verified
<?php
// Load environment variables from .env file (for local development)
function loadEnv($filePath) {
if (!file_exists($filePath)) {
return false;
}
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue; // Skip comments
}
if (strpos($line, '=') === false) {
continue; // Skip lines without =
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
// Remove quotes if present
if (preg_match('/^"(.*)"$/', $value, $matches)) {
$value = $matches[1];
} elseif (preg_match("/^'(.*)'$/", $value, $matches)) {
$value = $matches[1];
}
// Only set if not already in environment (Hugging Face Space variables take precedence)
if (!array_key_exists($name, $_ENV) && !isset($_SERVER[$name])) {
$_ENV[$name] = $value;
putenv("$name=$value");
}
}
return true;
}
// Load .env file for local development (Hugging Face Space already has environment variables)
if (file_exists(__DIR__ . '/.env')) {
loadEnv(__DIR__ . '/.env');
} else {
// Try to load from .env.example as fallback
if (file_exists(__DIR__ . '/.env.example')) {
loadEnv(__DIR__ . '/.env.example');
}
}
// Configuration management class
class StorageConfig {
public static function getUsers() {
// Default users if no env variables found
$defaultUsers = [
'PS01' => 'password123',
'PS02' => 'password456',
'admin' => 'admin123'
];
$users = [];
$i = 1;
while (true) {
// Check both $_ENV and $_SERVER for Hugging Face Space compatibility
$username = $_ENV["USER_{$i}_NAME"] ?? $_SERVER["USER_{$i}_NAME"] ?? '';
$password = $_ENV["USER_{$i}_PASSWORD"] ?? $_SERVER["USER_{$i}_PASSWORD"] ?? '';
if (empty($username) || empty($password)) {
break;
}
$users[$username] = $password;
$i++;
}
// If no users found in env, use defaults
return empty($users) ? $defaultUsers : $users;
}
public static function getStorageType() {
return $_ENV['STORAGE_TYPE'] ?? $_SERVER['STORAGE_TYPE'] ?? 'github';
}
public static function getGitHubConfig() {
// Read from environment variables (Hugging Face Space or system environment)
$config = [
'token' => $_ENV['GITHUB_TOKEN'] ?? $_SERVER['GITHUB_TOKEN'] ?? '',
'owner' => $_ENV['GITHUB_OWNER'] ?? $_SERVER['GITHUB_OWNER'] ?? '',
'repo' => $_ENV['GITHUB_REPO'] ?? $_SERVER['GITHUB_REPO'] ?? '',
'branch' => $_ENV['GITHUB_BRANCH'] ?? $_SERVER['GITHUB_BRANCH'] ?? 'main',
'path' => $_ENV['GITHUB_PATH'] ?? $_SERVER['GITHUB_PATH'] ?? 'pages/'
];
// Debug log for Hugging Face Space
error_log("GitHub Config Debug - Token present: " . (!empty($config['token']) ? 'YES' : 'NO'));
error_log("GitHub Config Debug - Owner: " . $config['owner']);
error_log("GitHub Config Debug - Repo: " . $config['repo']);
error_log("GitHub Config Debug - Branch: " . $config['branch']);
error_log("GitHub Config Debug - Path: " . $config['path']);
return $config;
}
public static function getKVConfig() {
return [
'api_key' => $_ENV['EDGEONE_KV_API_KEY'] ?? $_SERVER['EDGEONE_KV_API_KEY'] ?? '',
'secret_key' => $_ENV['EDGEONE_KV_SECRET_KEY'] ?? $_SERVER['EDGEONE_KV_SECRET_KEY'] ?? '',
'zone_id' => $_ENV['EDGEONE_KV_ZONE_ID'] ?? $_SERVER['EDGEONE_KV_ZONE_ID'] ?? '',
'namespace' => $_ENV['EDGEONE_KV_NAMESPACE'] ?? $_SERVER['EDGEONE_KV_NAMESPACE'] ?? 'vvvebjs',
'endpoint' => $_ENV['EDGEONE_KV_ENDPOINT'] ?? $_SERVER['EDGEONE_KV_ENDPOINT'] ?? 'https://edgeone.tencentcloudapi.com'
];
}
public static function getCurrentUser() {
// Check session first
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['username'])) {
return $_SESSION['username'];
}
// Fallback to Basic Auth
return $_SERVER['PHP_AUTH_USER'] ?? 'anonymous';
}
public static function getUserPath($username = null) {
if ($username === null) {
$username = self::getCurrentUser();
}
// Sanitize username for safe path usage
$safeUsername = preg_replace('/[^a-zA-Z0-9_-]/', '_', $username);
return "users/{$safeUsername}/";
}
// Add method to check if running on Hugging Face Space
public static function isHuggingFaceSpace() {
return isset($_SERVER['SPACE_ID']) ||
isset($_ENV['SPACE_ID']) ||
strpos($_SERVER['HTTP_HOST'] ?? '', '.hf.space') !== false;
}
// Debug method to show all environment variables (for debugging)
public static function debugEnvironment() {
$debug = [
'is_hf_space' => self::isHuggingFaceSpace(),
'space_id' => $_SERVER['SPACE_ID'] ?? $_ENV['SPACE_ID'] ?? 'not_set',
'host' => $_SERVER['HTTP_HOST'] ?? 'unknown',
'github_vars' => [
'token_set' => !empty($_ENV['GITHUB_TOKEN'] ?? $_SERVER['GITHUB_TOKEN']),
'owner' => $_ENV['GITHUB_OWNER'] ?? $_SERVER['GITHUB_OWNER'] ?? 'not_set',
'repo' => $_ENV['GITHUB_REPO'] ?? $_SERVER['GITHUB_REPO'] ?? 'not_set',
'branch' => $_ENV['GITHUB_BRANCH'] ?? $_SERVER['GITHUB_BRANCH'] ?? 'not_set',
'path' => $_ENV['GITHUB_PATH'] ?? $_SERVER['GITHUB_PATH'] ?? 'not_set'
]
];
error_log("Environment Debug: " . json_encode($debug));
return $debug;
}
}
// External storage handlers
class KVStorage {
private $config;
private $userPath;
public function __construct($config) {
$this->config = $config;
$this->userPath = StorageConfig::getUserPath();
}
public function save($key, $content) {
if (empty($this->config['api_key'])) {
error_log("KV Save Error: API key is missing");
return false;
}
if (empty($this->config['zone_id']) || empty($this->config['namespace'])) {
error_log("KV Save Error: Zone ID or namespace is missing");
return false;
}
// Add user path to key
$userKey = $this->userPath . $key;
// Use correct EdgeOne KV API endpoints
$url = $this->config['endpoint'];
$params = [
'Action' => 'CreatePurgeTask', // 这需要根据实际的EdgeOne KV API调整
'Version' => '2022-09-01',
'Region' => 'ap-beijing',
'ZoneId' => $this->config['zone_id'],
'Namespace' => $this->config['namespace'],
'Key' => $userKey,
'Value' => base64_encode($content),
'TTL' => 86400 // 24 hours TTL
];
error_log("KV Save Debug: Attempting to save key '$userKey' to namespace '{$this->config['namespace']}'");
$result = $this->makeRequest($url, $params);
if ($result) {
error_log("KV Save Success: Key saved successfully");
return true;
} else {
error_log("KV Save Error: API request failed");
return false;
}
}
public function get($key) {
if (empty($this->config['api_key'])) {
error_log("KV Get Error: API key is missing");
return false;
}
// Add user path to key
$userKey = $this->userPath . $key;
$url = $this->config['endpoint'];
$params = [
'Action' => 'DescribeOriginGroup', // 这需要根据实际的EdgeOne KV API调整
'Version' => '2022-09-01',
'Region' => 'ap-beijing',
'ZoneId' => $this->config['zone_id'],
'Namespace' => $this->config['namespace'],
'Key' => $userKey
];
error_log("KV Get Debug: Attempting to get key '$userKey' from namespace '{$this->config['namespace']}'");
$result = $this->makeRequest($url, $params);
if ($result && isset($result['Value'])) {
return base64_decode($result['Value']);
}
error_log("KV Get Error: Key not found or API request failed");
return false;
}
public function listUserFiles() {
if (empty($this->config['api_key'])) return [];
$url = $this->config['endpoint'];
$params = [
'Action' => 'DescribeOriginGroups', // 这需要根据实际的EdgeOne KV API调整
'Version' => '2022-09-01',
'Region' => 'ap-beijing',
'ZoneId' => $this->config['zone_id'],
'Namespace' => $this->config['namespace'],
'Prefix' => $this->userPath
];
$result = $this->makeRequest($url, $params);
if ($result && isset($result['Keys']) && is_array($result['Keys'])) {
$files = [];
foreach ($result['Keys'] as $item) {
if (isset($item['Key'])) {
$relativePath = str_replace($this->userPath, '', $item['Key']);
$files[] = [
'name' => basename($relativePath),
'path' => $relativePath,
'size' => $item['Size'] ?? 0,
'url' => null
];
}
}
return $files;
}
return [];
}
private function makeRequest($url, $params) {
// 添加通用的腾讯云API参数
$commonParams = [
'Timestamp' => time(),
'Nonce' => rand(10000, 99999),
'SecretId' => $this->config['api_key'],
'SignatureMethod' => 'HmacSHA256'
];
$params = array_merge($params, $commonParams);
// 生成签名 (这是简化版本,实际需要按照腾讯云API签名规范)
ksort($params);
$queryString = http_build_query($params);
$signature = hash_hmac('sha256', $queryString, $this->config['secret_key'], true);
$params['Signature'] = base64_encode($signature);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'User-Agent: VvvebJs-WebBuilder/1.0'
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("KV API cURL Error: $error");
return false;
}
if ($httpCode >= 200 && $httpCode < 300) {
$decoded = json_decode($result, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
error_log("KV API JSON Error: " . json_last_error_msg());
} else {
error_log("KV API HTTP Error: $httpCode - Response: $result");
}
return false;
}
}
class GitHubStorage {
private $config;
private $actualBranch = null;
private $userPath;
public function __construct($config) {
$this->config = $config;
$this->userPath = StorageConfig::getUserPath();
// Auto-detect the actual default branch
$this->detectDefaultBranch();
}
private function detectDefaultBranch() {
if (empty($this->config['token']) || empty($this->config['owner']) || empty($this->config['repo'])) {
return;
}
// Get repository info to find the default branch
$url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}";
$repoInfo = $this->makeRequest($url, 'GET');
if ($repoInfo && isset($repoInfo['default_branch'])) {
$this->actualBranch = $repoInfo['default_branch'];
error_log("GitHub: Detected default branch as '{$this->actualBranch}'");
} else {
// Fallback: try common branch names
$commonBranches = ['main', 'master', 'develop'];
foreach ($commonBranches as $branch) {
if ($this->checkBranchExists($branch)) {
$this->actualBranch = $branch;
error_log("GitHub: Found existing branch '{$branch}'");
break;
}
}
}
// Update config with detected branch
if ($this->actualBranch) {
$this->config['branch'] = $this->actualBranch;
}
}
private function checkBranchExists($branchName) {
$url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/branches/{$branchName}";
$result = $this->makeRequest($url, 'GET');
return $result !== false;
}
public function save($filename, $content) {
error_log("GitHubStorage: Starting save process for $filename");
// Get user path
$userPath = $this->getUserPath();
if (!$userPath) {
error_log("GitHubStorage: Failed to get user path");
return false;
}
$filePath = $userPath . '/' . $filename;
error_log("GitHubStorage: Full file path: $filePath");
// Ensure user directory exists BEFORE attempting to save
if (!$this->ensureUserDirectoryExists($userPath)) {
error_log("GitHubStorage: Failed to ensure user directory exists");
return false;
}
// Wait a moment for GitHub API to propagate the directory creation
usleep(500000); // 0.5 seconds
// Now attempt to save the file
$url = $this->apiUrl . "/contents/" . $filePath;
// Check if file already exists to get its SHA
$existingFile = $this->makeRequest($url);
$sha = null;
if ($existingFile !== false && isset($existingFile['sha'])) {
$sha = $existingFile['sha'];
error_log("GitHubStorage: Found existing file with SHA: $sha");
} else {
error_log("GitHubStorage: Creating new file");
}
$data = [
'message' => $sha ? "Update $filename" : "Create $filename",
'content' => base64_encode($content),
'branch' => $this->branch
];
if ($sha) {
$data['sha'] = $sha;
}
error_log("GitHubStorage: Sending save request to GitHub API");
$result = $this->makeRequest($url, 'PUT', $data);
if ($result !== false) {
error_log("GitHubStorage: File saved successfully");
return true;
} else {
error_log("GitHubStorage: Failed to save file");
return false;
}
}
private function ensureUserDirectoryExists($userPath) {
error_log("Ensuring user directory exists: $userPath");
// Split the path into components
$pathParts = explode('/', trim($userPath, '/'));
$currentPath = '';
// Create each directory level incrementally
foreach ($pathParts as $part) {
if (empty($part)) continue;
$currentPath .= ($currentPath ? '/' : '') . $part;
// Check if this directory level exists
$url = $this->apiUrl . "/contents/" . $currentPath;
$response = $this->makeRequest($url);
if ($response === false) {
// Directory doesn't exist, create it with a placeholder file
error_log("Creating directory level: $currentPath");
$placeholderFile = $currentPath . '/.gitkeep';
$createUrl = $this->apiUrl . "/contents/" . $placeholderFile;
$createData = [
'message' => "Create directory: $currentPath",
'content' => base64_encode(''),
'branch' => $this->branch
];
$createResponse = $this->makeRequest($createUrl, 'PUT', $createData);
if ($createResponse === false) {
error_log("Failed to create directory level: $currentPath");
return false;
} else {
error_log("Successfully created directory level: $currentPath");
}
} else {
error_log("Directory level already exists: $currentPath");
}
}
error_log("User directory structure ensured: $userPath");
return true;
}
public function get($filename) {
if (empty($this->config['token'])) return false;
// Add user path to filename
$userFilename = $this->userPath . $filename;
$path = $this->config['path'] . $userFilename;
$url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
$result = $this->makeRequest($url, 'GET');
if ($result && isset($result['content'])) {
return base64_decode($result['content']);
}
return false;
}
public function listUserFiles() {
if (empty($this->config['token'])) return [];
$userDir = $this->config['path'] . $this->userPath;
$url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$userDir}";
error_log("GitHub listUserFiles: Requesting $url");
$result = $this->makeRequest($url, 'GET');
if ($result && is_array($result)) {
$files = [];
foreach ($result as $item) {
if ($item['type'] === 'file') {
// Remove user path prefix from filename
$relativePath = str_replace($this->userPath, '', $item['path']);
$relativePath = str_replace($this->config['path'], '', $relativePath);
$files[] = [
'name' => $item['name'],
'path' => $relativePath,
'size' => $item['size'],
'url' => $item['download_url']
];
}
}
error_log("GitHub listUserFiles: Found " . count($files) . " files");
return $files;
} else {
error_log("GitHub listUserFiles: No files found or directory doesn't exist");
// If directory doesn't exist, return empty array instead of error
return [];
}
}
public function delete($filename) {
if (empty($this->config['token'])) return false;
// Add user path to filename
$userFilename = $this->userPath . $filename;
$path = $this->config['path'] . $userFilename;
$url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
// Get current file SHA
$sha = $this->getFileSHA($path);
if (!$sha) return false;
$data = [
'message' => "Delete {$userFilename} via VvvebJs",
'sha' => $sha,
'branch' => $this->config['branch']
];
$result = $this->makeRequest($url, 'DELETE', $data);
return $result !== false;
}
private function getFileSHA($path) {
$url = "https://api.github.com/repos/{$this->config['owner']}/{$this->config['repo']}/contents/{$path}";
$result = $this->makeRequest($url, 'GET');
return $result && isset($result['sha']) ? $result['sha'] : null;
}
private function makeRequest($url, $method = 'GET', $data = null) {
$headers = [
'Authorization: token ' . $this->config['token'],
'User-Agent: VvvebJs-WebBuilder',
'Accept: application/vnd.github.v3+json',
'Content-Type: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
if ($method === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
}
// Enhanced logging for debugging
$debugUrl = preg_replace('/\/contents\/.*/', '/contents/[PATH_HIDDEN]', $url);
error_log("GitHub API Request: $method $debugUrl");
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
error_log("GitHub API Response: HTTP $httpCode");
if ($curlError) {
error_log("GitHub API cURL Error: $curlError");
return false;
}
if ($httpCode >= 200 && $httpCode < 300) {
$decoded = json_decode($response, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
error_log("GitHub API JSON Decode Error: " . json_last_error_msg());
error_log("GitHub API Raw Response: " . substr($response, 0, 500));
return false;
}
return $decoded;
} else {
// Enhanced error logging for debugging
$errorInfo = json_decode($response, true);
if ($errorInfo && isset($errorInfo['message'])) {
error_log("GitHub API Error ($httpCode): " . $errorInfo['message']);
if (isset($errorInfo['documentation_url'])) {
error_log("GitHub API Documentation: " . $errorInfo['documentation_url']);
}
} else {
error_log("GitHub API Error ($httpCode): " . substr($response, 0, 500));
}
// Special handling for 404 errors - they might be expected when checking if files exist
if ($httpCode === 404 && $method === 'GET') {
error_log("GitHub API: Resource not found (this may be expected when checking file existence)");
}
return false;
}
}
}
// Storage manager with user isolation
class StorageManager {
private $kvStorage;
private $githubStorage;
private $storageType;
private $currentUser;
public function __construct() {
$this->storageType = StorageConfig::getStorageType();
$this->currentUser = StorageConfig::getCurrentUser();
if (in_array($this->storageType, ['kv', 'both'])) {
$this->kvStorage = new KVStorage(StorageConfig::getKVConfig());
}
if (in_array($this->storageType, ['github', 'both'])) {
$this->githubStorage = new GitHubStorage(StorageConfig::getGitHubConfig());
}
}
public function saveFile($filename, $content) {
// Ensure filename is safe and doesn't contain path traversal
$filename = $this->sanitizeFilename($filename);
error_log("StorageManager saveFile: Starting save for file '$filename'");
error_log("StorageManager saveFile: Storage type is '{$this->storageType}'");
error_log("StorageManager saveFile: Current user is '{$this->currentUser}'");
$success = false;
$errors = [];
if ($this->githubStorage) {
error_log("StorageManager saveFile: Attempting GitHub save...");
$githubSuccess = $this->githubStorage->save($filename, $content);
if ($githubSuccess) {
error_log("StorageManager saveFile: GitHub save SUCCESS");
$success = true;
} else {
error_log("StorageManager saveFile: GitHub save FAILED");
$errors[] = "GitHub save failed";
}
} else {
error_log("StorageManager saveFile: GitHub storage not configured");
}
if ($this->kvStorage) {
error_log("StorageManager saveFile: Attempting KV save...");
$kvSuccess = $this->kvStorage->save($filename, $content);
if ($kvSuccess) {
error_log("StorageManager saveFile: KV save SUCCESS");
$success = true;
} else {
error_log("StorageManager saveFile: KV save FAILED");
$errors[] = "KV save failed";
}
} else {
error_log("StorageManager saveFile: KV storage not configured");
}
if (!$success && !empty($errors)) {
error_log("StorageManager saveFile: All saves failed: " . implode(", ", $errors));
}
return $success;
}
public function getFile($filename) {
// Ensure filename is safe and doesn't contain path traversal
$filename = $this->sanitizeFilename($filename);
error_log("StorageManager getFile: Starting load for file '$filename'");
error_log("StorageManager getFile: Storage type is '{$this->storageType}'");
error_log("StorageManager getFile: Current user is '{$this->currentUser}'");
if ($this->githubStorage) {
error_log("StorageManager getFile: Attempting GitHub load...");
$content = $this->githubStorage->get($filename);
if ($content !== false) {
error_log("StorageManager getFile: GitHub load SUCCESS");
return $content;
} else {
error_log("StorageManager getFile: GitHub load FAILED");
}
} else {
error_log("StorageManager getFile: GitHub storage not configured");
}
if ($this->kvStorage) {
error_log("StorageManager getFile: Attempting KV load...");
$content = $this->kvStorage->get($filename);
if ($content !== false) {
error_log("StorageManager getFile: KV load SUCCESS");
return $content;
} else {
error_log("StorageManager getFile: KV load FAILED");
}
} else {
error_log("StorageManager getFile: KV storage not configured");
}
error_log("StorageManager getFile: All load attempts failed");
return false;
}
public function listFiles() {
$files = [];
if ($this->githubStorage) {
$githubFiles = $this->githubStorage->listUserFiles();
$files = array_merge($files, $githubFiles);
}
if ($this->kvStorage) {
$kvFiles = $this->kvStorage->listUserFiles();
$files = array_merge($files, $kvFiles);
}
return $files;
}
public function deleteFile($filename) {
// Ensure filename is safe and doesn't contain path traversal
$filename = $this->sanitizeFilename($filename);
$success = false;
if ($this->githubStorage) {
$success = $this->githubStorage->delete($filename) || $success;
}
// KV storage delete would need to be implemented based on provider
return $success;
}
public function getCurrentUser() {
return $this->currentUser;
}
public function getUserPath() {
return StorageConfig::getUserPath();
}
private function sanitizeFilename($filename) {
// Remove any path traversal attempts
$filename = str_replace(['../', '..\\', '../', '..\\'], '', $filename);
// Remove leading slashes
$filename = ltrim($filename, '/\\');
// Ensure safe characters only
$filename = preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '_', $filename);
return $filename;
}
}