commit 01d99c50542f06002d7c21a9fbeb27588b9bf1bf Author: din9xtr Date: Sat Jan 10 01:24:08 2026 +0700 Initial commit: Cloud Control Panel diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eaca763 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +STORAGE_PATH=storage +STORAGE_USER_LIMIT_GB=70 +USER=admin +PASSWORD=admin \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fbc6f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +.idea/ +.vscode/ +*.swp +*.swo + +*.com +*.class +*.dll +*.exe +*.o +*.so + +node_modules/ +vendor/ + +/logs/* +/logs/**/* +*.log +!logs/.gitkeep + +/storage/* +/storage/**/* +!storage/.gitkeep + +/db/database.sqlite +/db/database.sqlite-journal +/db/database.sqlite-wal +/db/database.sqlite-shm +/db/*.sqlite* + +!/db/.gitkeep +!/db/migration/ +!/db/migrate.php + +.env +.env.local +.env.*.local + +.DS_Store +Thumbs.db + +.cache/ +.phpunit.result.cache +.phpstan/cache/ +.phpstan/resultCache.php +.phpstan +tmp/ +temp/ + +/uploads/ + +.dockerignore +docker-compose.override.yml + +coverage/ + +composer.phar +composer.lock +*.cache +.sass-cache/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..999b365 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM php:8.5-cli-alpine +RUN apk add --no-cache libzip-dev zip \ + && docker-php-ext-install zip +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +WORKDIR /var/www +COPY . . +CMD ["php", "-S", "0.0.0.0:8001", "-t", "public"] \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..17deffe --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Grudinin Andrew (din9xtr) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..3e67a47 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: build up down install logs bash migrate + + +build: + docker compose build --no-cache + +up: + docker compose up -d + +down: + docker compose down + +install: + docker compose run --rm \ + -v $(PWD):/var/www \ + app composer install + +logs: + docker compose logs -f + +bash: + docker compose exec app sh + +migrate: + docker compose exec app php /var/www/db/migrate.php \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fda993a --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Cloud control panel + +A lightweight, self-hosted cloud management panel designed for simplicity and performance. Built with modern PHP and +containerized for easy deployment, it provides an intuitive interface for managing your personal cloud storage with +minimal resource overhead. + +## ✨ Features + +1. Minimal footprint - Low memory and CPU usage + +2. Docker-first - Easy deployment with containerization +3. Modern stack - Built with PHP 8+ and clean architecture + +4. File management - Upload, organize, and share files + +5. Responsive UI - Pure CSS and vanilla JavaScript, no framework dependencies + +## 🚀 Quick Start + +### Prerequisites: + +Docker and Docker Compose, Make utility + +Configure Environment Variables + +```bash + +cp .env.example .env +nano .env +``` + +Build and Deploy + +```bash + +make build +make install +make up +make migrate +``` + +## 🌐 Access + +Web Interface: http://localhost:8001 (or any configured port) + +### ⚙️ Additional Commands + +```bash + +make bash +# in docker environment +composer analyse +composer test +``` + +### 📄 Note: For production use, ensure to: + +1. Change default credentials + +2. Configure SSL/TLS + +3. Set up regular backups + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..c94d614 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "require": { + "php": "^8.5", + "nikic/fast-route": "^1.3", + "monolog/monolog": "^3", + "nyholm/psr7": "^1.3.2", + "nyholm/psr7-server": "^1.0", + "psr/container": "^2.0", + "psr/http-server-middleware": "^1.0", + "relay/relay": "^3.0", + "ext-pdo": "*", + "ext-fileinfo": "*", + "ext-zip": "*" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/extension-installer": "^1.4", + "phpunit/phpunit": "^10.5", + "friendsofphp/php-cs-fixer": "^3.92" + }, + "autoload": { + "psr-4": { + "Din9xtrCloud\\": "src/" + }, + "files": [ + "src/Helpers/helpers.php" + ] + }, + "scripts": { + "test": "phpunit", + "analyse": "phpstan analyse src --level=8", + "analyse-strict": "phpstan analyse src --level=max", + "coverage": "phpunit --coverage-html coverage", + "cs-check": "php-cs-fixer fix --dry-run --diff", + "cs-fix": "php-cs-fixer fix" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } + } +} diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/migrate.php b/db/migrate.php new file mode 100755 index 0000000..3addd57 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,72 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$db->exec(" +CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + migration TEXT NOT NULL UNIQUE, + ran_at INTEGER NOT NULL +); +"); + +$migrations = glob($migrationsPath . '/*.php'); +sort($migrations); + +foreach ($migrations as $file) { + $name = basename($file); + + $stmt = $db->prepare( + "SELECT COUNT(*) FROM migrations WHERE migration = :migration" + ); + $stmt->execute(['migration' => $name]); + + if ((int)$stmt->fetchColumn() > 0) { + continue; + } + + echo "Running: $name\n"; + + /** @var callable $migration */ + $migration = require $file; + + if (!is_callable($migration)) { + throw new RuntimeException("Migration $name must return callable"); + } + + $db->beginTransaction(); + try { + $migration($db); + + $stmt = $db->prepare(" + INSERT INTO migrations (migration, ran_at) + VALUES (:migration, :ran_at) + "); + $stmt->execute([ + 'migration' => $name, + 'ran_at' => time(), + ]); + + $db->commit(); + echo "✔ Migrated: $name\n"; + } catch (Throwable $e) { + $db->rollBack(); + echo "✖ Failed: $name\n"; + throw $e; + } +} diff --git a/db/migration/001_create_users.php b/db/migration/001_create_users.php new file mode 100755 index 0000000..320d1dd --- /dev/null +++ b/db/migration/001_create_users.php @@ -0,0 +1,37 @@ +exec(" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + "); + $stmt = $db->prepare( + "SELECT COUNT(*) FROM users WHERE username = :username" + ); + $stmt->execute(['username' => getenv('USER')]); + + if ((int)$stmt->fetchColumn() > 0) { + return; + } + + $passwordHash = password_hash( + getenv('PASSWORD'), + PASSWORD_DEFAULT + ); + + $stmt = $db->prepare(" + INSERT INTO users (username, password, created_at) + VALUES (:username, :password, :created_at) + "); + + $stmt->execute([ + 'username' => $_ENV['USER'], + 'password' => $passwordHash, + 'created_at' => time(), + ]); +}; \ No newline at end of file diff --git a/db/migration/002_create_sessions.php b/db/migration/002_create_sessions.php new file mode 100755 index 0000000..1587442 --- /dev/null +++ b/db/migration/002_create_sessions.php @@ -0,0 +1,18 @@ +exec(" + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + auth_token TEXT NOT NULL UNIQUE, + ip TEXT, + user_agent TEXT, + created_at INTEGER NOT NULL, + last_activity_at INTEGER NOT NULL, + revoked_at INTEGER, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + "); +}; diff --git a/db/migration/003_create_login_throttle.php b/db/migration/003_create_login_throttle.php new file mode 100755 index 0000000..6900fc3 --- /dev/null +++ b/db/migration/003_create_login_throttle.php @@ -0,0 +1,13 @@ +exec(" + CREATE TABLE IF NOT EXISTS login_throttle ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOT NULL, + attempts INTEGER NOT NULL DEFAULT 1, + last_attempt INTEGER NOT NULL + ); + "); +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..c70f887 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: cloud + ports: + - "8001:8001" + volumes: + - .:/var/www + - ./db:/var/db + - ./php/php.ini:/usr/local/etc/php/conf.d/uploads.ini + env_file: + - .env \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/php/php.ini b/php/php.ini new file mode 100644 index 0000000..1f8173c --- /dev/null +++ b/php/php.ini @@ -0,0 +1,50 @@ +; --------------------------------------------------------------------- +; PHP Configuration for File Uploads (< 512M) +; --------------------------------------------------------------------- + +; Error Reporting (for development only!) +display_errors = 0 +error_reporting = E_ALL +log_errors = On +error_log = /var/log/php_errors.log + +; File Upload Settings +upload_max_filesize = 512M +post_max_size = 565M +max_file_uploads = 10 +max_execution_time = 300 +max_input_time = 300 +memory_limit = 256M +file_uploads = On +fastcgi_request_buffering off; + +; Session Settings +session.gc_maxlifetime = 14400 +session.cookie_lifetime = 14400 +session.use_strict_mode = 1 +session.cookie_httponly = 1 +session.cookie_secure = 1 +session.cookie_samesite = Strict + +; Security Settings +expose_php = Off +allow_url_include = Off +allow_url_fopen = Off + +; Character Encoding +default_charset = UTF-8 +internal_encoding = UTF-8 +output_encoding = UTF-8 + +; Output Buffering +output_buffering = 4096 + +; Realpath Cache +realpath_cache_size = 4096K +realpath_cache_ttl = 120 + +; OpCache (for production) +; opcache.enable=1 +; opcache.memory_consumption=128 +; opcache.max_accelerated_files=10000 +; opcache.revalidate_freq=2 \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100755 index 0000000..5de9bfb --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,78 @@ +parameters: + level: 8 + paths: + - src/ + excludePaths: + - tests/* + - vendor/* + fileExtensions: + - php + phpVersion: 80500 + treatPhpDocTypesAsCertain: true + parallel: + maximumNumberOfProcesses: 4 + processTimeout: 300.0 + jobSize: 20 + ignoreErrors: + - '#Method .*::.*\(\) has no return typehint specified.#' + - '#Property .* has no typehint specified.#' + - '#Parameter .* has no typehint specified.#' + - '#Access to an undefined property.*#' + - '#Class .* not found.#' + + reportUnmatchedIgnoredErrors: false + + universalObjectCratesClasses: + - stdClass + - SimpleXMLElement + + bootstrapFiles: + - vendor/autoload.php + + scanFiles: + - vendor/autoload.php + + tmpDir: .phpstan/cache + + checkExplicitMixedMissingReturn: false + checkFunctionNameCase: false + checkInternalClassCaseSensitivity: false + reportMaybesInMethodSignatures: false + reportMaybesInPropertyPhpDocTypes: false + reportStaticMethodSignatures: false + checkTooWideReturnTypesInProtectedAndPublicMethods: false + checkUninitializedProperties: true + checkDynamicProperties: false + rememberPossiblyImpureFunctionValues: true + checkBenevolentUnionTypes: false + reportPossiblyNonexistentGeneralArrayOffset: false + reportPossiblyNonexistentConstantArrayOffset: false + reportAlwaysTrueInLastCondition: false + reportWrongPhpDocTypeInVarTag: false + reportAnyTypeWideningInVarTag: false + checkMissingOverrideMethodAttribute: false + checkStrictPrintfPlaceholderTypes: false + checkMissingCallableSignature: false + + exceptions: + implicitThrows: false + uncheckedExceptionRegexes: + - '#^Exception$#' + - '#^Error$#' + - '#^RuntimeException$#' + - '#^LogicException$#' + checkedExceptionClasses: [] + + tips: + treatPhpDocTypesAsCertain: false + tipsOfTheDay: true + + additionalConstructors: [] + + polluteScopeWithLoopInitialAssignments: true + polluteScopeWithAlwaysIterableForeach: true + polluteScopeWithBlock: true + + inferPrivatePropertyTypeFromConstructor: false + +includes: diff --git a/public/assets/cloud.css b/public/assets/cloud.css new file mode 100755 index 0000000..349ab50 --- /dev/null +++ b/public/assets/cloud.css @@ -0,0 +1,2304 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +body { + background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%); + color: #2d3748; + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 1rem; + position: relative; +} + +header { + background: rgba(255, 255, 255, 0.98); + color: #4a5568; + padding: 1rem 2rem; + width: 100%; + max-width: 1200px; + text-align: center; + font-weight: 700; + font-size: 1.5rem; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); + position: fixed; + top: 0; + z-index: 1000; + border-radius: 0 0 20px 20px; + margin: 0 auto; + left: 50%; + transform: translateX(-50%); +} + +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + margin: 0 auto; +} + +.navbar-brand { + color: transparent; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + background-clip: text; + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.5px; +} + +footer { + text-align: center; + padding: 1rem; + font-size: 0.85rem; + background: transparent; + width: 100%; + color: #718096; + margin-top: auto; + position: relative; + z-index: 1; +} + +footer p { + opacity: 0.7; + transition: opacity 0.3s ease; +} + +footer:hover p { + opacity: 1; +} + +main.container { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + width: 100%; + max-width: 1200px; + padding: 2rem; + margin: 80px auto 40px; + position: relative; + z-index: 2; +} + +.login-card { + background: rgba(255, 255, 255, 0.98); + padding: 2.5rem; + border-radius: 24px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08), + 0 8px 24px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + width: 100%; + max-width: 420px; + text-align: center; + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.3); + animation: cardEntrance 0.6s cubic-bezier(0.22, 1, 0.36, 1); +} + +@keyframes cardEntrance { + from { + opacity: 0; + transform: translateY(20px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.login-card h1 { + margin-bottom: 2rem; + color: transparent; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + background-clip: text; + font-size: 2.25rem; + font-weight: 800; + letter-spacing: -0.5px; +} + +.login-card .error { + color: #fc8181; + margin-bottom: 1.25rem; + padding: 0.75rem 1rem; + background: rgba(252, 129, 129, 0.08); + border-radius: 12px; + border-left: 4px solid #fc8181; + font-weight: 500; + animation: shake 0.5s ease-in-out; +} + +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-4px); + } + 75% { + transform: translateX(4px); + } +} + +.login-form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.login-form label { + display: block; + margin-bottom: 0.5rem; + text-align: left; + font-weight: 600; + color: #4a5568; + font-size: 0.95rem; + padding-left: 0.25rem; +} + +.login-form input { + width: 100%; + padding: 1rem 1.25rem; + margin-bottom: 0.25rem; + border: 2px solid #e2e8f0; + border-radius: 14px; + font-size: 1rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: rgba(247, 250, 252, 0.8); + color: #2d3748; +} + +.login-form input:hover { + border-color: #cbd5e0; + background: white; +} + +.login-form input:focus { + border-color: #667eea; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15), + 0 4px 12px rgba(102, 126, 234, 0.1); + outline: none; + background: white; + transform: translateY(-1px); +} + +.login-form input::placeholder { + color: #a0aec0; +} + + +.login-form button { + width: 100%; + padding: 1rem; + border: none; + border-radius: 14px; + font-size: 1.1rem; + font-weight: 700; + cursor: pointer; + color: white; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + margin-top: 1rem; +} + +.login-form button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.7s; +} + +.login-form button:hover { + transform: translateY(-3px); + box-shadow: 0 12px 24px rgba(102, 126, 234, 0.3), + 0 6px 12px rgba(118, 75, 162, 0.2); +} + +.login-form button:hover::before { + left: 100%; +} + +.login-form button:active { + transform: translateY(-1px); + box-shadow: 0 6px 12px rgba(102, 126, 234, 0.25), + 0 3px 6px rgba(118, 75, 162, 0.2); +} + +.login-form button:focus { + outline: none; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.3), + 0 12px 24px rgba(102, 126, 234, 0.25); +} + +@media (max-width: 768px) { + body { + padding: 0.5rem; + } + + body::before { + height: 220px; + border-radius: 0 0 24px 24px; + } + + header { + padding: 0.75rem 1rem; + font-size: 1.25rem; + border-radius: 0 0 16px 16px; + max-width: 100%; + width: 100%; + left: 0; + transform: none; + } + + .navbar-brand { + font-size: 1.5rem; + } + + main.container { + margin-top: 70px; + padding: 1rem; + } + + .login-card { + padding: 2rem 1.5rem; + margin: 0 auto; + max-width: 380px; + } + + footer { + padding: 0.75rem; + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .login-card { + padding: 1.75rem 1.25rem; + } + + .login-card h1 { + font-size: 1.9rem; + margin-bottom: 1.5rem; + } + + .login-form input { + padding: 0.875rem 1rem; + } + + .login-form button { + padding: 0.875rem; + font-size: 1rem; + } +} + +.full-height { + margin-top: 0 !important; + min-height: 100vh; + justify-content: center; +} + +/* ========== Стили для страниц ошибок ========== */ +.error-container { + background: rgba(255, 255, 255, 0.98); + padding: 3.5rem 2.5rem; + border-radius: 28px; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.08), + 0 10px 30px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + width: 100%; + max-width: 480px; + text-align: center; + backdrop-filter: blur(25px); + -webkit-backdrop-filter: blur(25px); + border: 1px solid rgba(255, 255, 255, 0.4); + animation: errorSlideIn 0.7s cubic-bezier(0.22, 1, 0.36, 1); + margin: 100px auto 60px; + position: relative; + overflow: hidden; +} + +@keyframes errorSlideIn { + from { + opacity: 0; + transform: translateY(30px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + + +.error-container::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 30% 30%, + rgba(102, 126, 234, 0.05) 0%, + rgba(118, 75, 162, 0.03) 25%, + transparent 50%); + z-index: -1; +} + +.error-code { + font-size: 7rem; + font-weight: 900; + line-height: 1; + margin-bottom: 0.5rem; + background: linear-gradient(135deg, + #667eea 0%, + #764ba2 50%, + #667eea 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + background-size: 200% 200%; + animation: gradientShift 3s ease infinite; + letter-spacing: -3px; + position: relative; + display: inline-block; + text-shadow: 0 5px 15px rgba(102, 126, 234, 0.15); +} + +@keyframes gradientShift { + 0%, 100% { + background-position: 0 50%; + } + 50% { + background-position: 100% 50%; + } +} + +.error-code::after { + content: ''; + position: absolute; + bottom: -12px; + left: 50%; + transform: translateX(-50%); + width: 80px; + height: 4px; + background: linear-gradient(90deg, + rgba(102, 126, 234, 0) 0%, + #667eea 50%, + rgba(102, 126, 234, 0) 100%); + border-radius: 2px; + animation: linePulse 2s ease-in-out infinite; +} + +@keyframes linePulse { + 0%, 100% { + width: 80px; + opacity: 1; + } + 50% { + width: 120px; + opacity: 0.7; + } +} + +.error-message { + font-size: 1.25rem; + line-height: 1.7; + color: #4a5568; + margin: 2rem 0 3rem; + padding: 0 1rem; + font-weight: 500; + position: relative; +} + +.action-buttons { + display: flex; + gap: 1.25rem; + justify-content: center; + margin-top: 2.5rem; + flex-wrap: wrap; +} + +.btn { + padding: 1rem 2.5rem; + border-radius: 16px; + font-size: 1.05rem; + font-weight: 700; + cursor: pointer; + text-decoration: none; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + min-width: 180px; + border: none; + position: relative; + overflow: hidden; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent); + transition: left 0.7s ease; +} + +.btn:hover::before { + left: 100%; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.25), + 0 4px 15px rgba(118, 75, 162, 0.2); +} + +.btn-primary:hover { + transform: translateY(-4px) scale(1.03); + box-shadow: 0 15px 35px rgba(102, 126, 234, 0.35), + 0 8px 25px rgba(118, 75, 162, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.2); +} + +.btn-primary:active { + transform: translateY(-2px) scale(1.01); + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3), + 0 5px 15px rgba(118, 75, 162, 0.25); +} + + +.btn-secondary { + background: rgba(255, 255, 255, 0.9); + color: #4a5568; + border: 2px solid #e2e8f0; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06); +} + +.btn-secondary:hover { + transform: translateY(-4px); + border-color: #cbd5e0; + background: white; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(255, 255, 255, 0.2); + color: #2d3748; +} + +.btn-secondary:active { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); +} + +.btn::after { + font-size: 1.1em; + transition: transform 0.3s ease; +} + +.btn:hover::after { + transform: translateX(2px); +} + +.btn-primary::after { + content: ''; +} + +.btn-secondary::after { + content: ''; +} + +.btn:focus { + outline: none; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.2), + 0 8px 25px rgba(102, 126, 234, 0.25); +} + +.btn-secondary:focus { + box-shadow: 0 0 0 4px rgba(160, 174, 192, 0.2), + 0 6px 20px rgba(0, 0, 0, 0.08); +} + +@media (max-width: 768px) { + .error-container { + margin: 80px auto 40px; + padding: 2.5rem 2rem; + max-width: 420px; + border-radius: 24px; + } + + .error-code { + font-size: 5.5rem; + } + + .error-message { + font-size: 1.15rem; + margin: 1.5rem 0 2.5rem; + } + + .action-buttons { + gap: 1rem; + margin-top: 2rem; + } + + .btn { + padding: 0.875rem 2rem; + min-width: 160px; + font-size: 1rem; + } +} + +@media (max-width: 480px) { + .error-container { + margin: 60px auto 30px; + padding: 2rem 1.5rem; + max-width: 360px; + border-radius: 20px; + } + + .error-code { + font-size: 4.5rem; + letter-spacing: -2px; + } + + .error-code::after { + width: 60px; + height: 3px; + } + + @keyframes linePulse { + 0%, 100% { + width: 60px; + } + 50% { + width: 90px; + } + } + + .error-message { + font-size: 1.05rem; + margin: 1.25rem 0 2rem; + padding: 0 0.5rem; + } + + .action-buttons { + flex-direction: column; + gap: 0.875rem; + width: 100%; + } + + .btn { + width: 100%; + min-width: auto; + padding: 0.875rem 1.5rem; + } + + .btn::after { + content: ''; + display: none; + } + + .btn-secondary::before, + .btn-primary::before { + margin-right: 0.5rem; + } +} + +@media (max-width: 360px) { + .error-container { + padding: 1.75rem 1.25rem; + margin: 50px auto 25px; + } + + .error-code { + font-size: 4rem; + } + + .error-message { + font-size: 1rem; + margin: 1rem 0 1.75rem; + } +} + +@media (prefers-color-scheme: dark) { + body { + background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); + color: #f1f5f9; + } + + /* Header */ + header { + background: rgba(30, 41, 59, 0.95); + color: #cbd5e1; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + } + + .navbar-brand { + background: linear-gradient(90deg, #818cf8 0%, #c084fc 100%); + -webkit-background-clip: text; + background-clip: text; + } + + /* Footer */ + footer { + color: #94a3b8; + } + + /* Login card */ + .login-card { + background: rgba(30, 41, 59, 0.95); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), + 0 8px 24px rgba(0, 0, 0, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.15); + } + + .login-card h1 { + background: linear-gradient(90deg, #818cf8 0%, #c084fc 100%); + -webkit-background-clip: text; + background-clip: text; + } + + /* Ошибки в форме */ + .login-card .error { + color: #fca5a5; + background: rgba(248, 113, 113, 0.12); + border-left-color: #f87171; + } + + /* Форма */ + .login-form label { + color: #cbd5e1; + } + + .login-form input { + background: rgba(30, 41, 59, 0.8); + border-color: #475569; + color: #f1f5f9; + } + + .login-form input:hover { + background: rgba(41, 53, 73, 0.9); + border-color: #64748b; + } + + .login-form input:focus { + border-color: #818cf8; + box-shadow: 0 0 0 4px rgba(129, 140, 248, 0.2), + 0 4px 12px rgba(129, 140, 248, 0.15); + background: rgba(41, 53, 73, 0.95); + } + + .login-form input::placeholder { + color: #94a3b8; + } + + /* Кнопка формы */ + .login-form button { + background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%); + } + + .login-form button:hover { + box-shadow: 0 12px 24px rgba(129, 140, 248, 0.25), + 0 6px 12px rgba(192, 132, 252, 0.2); + } + + .login-form button:focus { + box-shadow: 0 0 0 4px rgba(129, 140, 248, 0.3), + 0 12px 24px rgba(129, 140, 248, 0.25); + } + + .error-container { + background: rgba(26, 32, 44, 0.98); + border-color: rgba(255, 255, 255, 0.1); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3), + 0 10px 30px rgba(0, 0, 0, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + + .error-message { + color: #cbd5e0; + } + + .btn-secondary { + background: rgba(45, 55, 72, 0.9); + color: #e2e8f0; + border-color: #4a5568; + } + + .btn-secondary:hover { + background: rgba(55, 65, 81, 0.95); + border-color: #718096; + color: #f7fafc; + } + + .error-container::before { + background: radial-gradient(circle at 30% 30%, + rgba(102, 126, 234, 0.08) 0%, + rgba(118, 75, 162, 0.05) 25%, + transparent 50%); + } +} + +/* ========== ДАШБОРД ========== */ + +.dashboard-container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 2rem; + animation: fadeIn 0.6s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2.5rem; + padding-bottom: 1.5rem; + border-bottom: 2px solid rgba(0, 0, 0, 0.05); +} + +.welcome-title { + font-size: 2.25rem; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + margin-bottom: 0.5rem; +} + +.welcome-subtitle { + color: #718096; + font-size: 1.1rem; + font-weight: 500; +} + + +.btn { + padding: 0.875rem 1.75rem; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + border: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); +} + +.btn-secondary { + background: white; + color: #4a5568; + border: 2px solid #e2e8f0; +} + +.btn-secondary:hover { + border-color: #cbd5e0; + background: #f7fafc; +} + +.btn-small { + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.btn-icon { + font-size: 1.1em; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-bottom: 3rem; +} + +.stat-card { + cursor: pointer; + text-decoration: none; + background: white; + padding: 1.5rem; + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 1.25rem; +} + +.stat-card:hover { + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); +} + +.stat-icon { + width: 60px; + height: 60px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + flex-shrink: 0; +} + +.stat-content { + flex: 1; +} + +.stat-title { + color: #718096; + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 0.25rem; + text-transform: uppercase; +} + +.stat-value { + font-size: 2rem; + font-weight: 800; + color: #2d3748; + margin-bottom: 0.5rem; +} + +.stat-progress { + margin-top: 0.5rem; +} + +.progress-bar { + height: 6px; + background: #e2e8f0; + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.25rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 3px; + transition: width 0.5s ease; +} + +.progress-text { + font-size: 0.85rem; + color: #718096; + font-weight: 500; +} + +/* Основной контент */ +.dashboard-content { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 2rem; + margin-bottom: 3rem; +} + +.chart-container { + background: white; + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.chart-title { + font-size: 1.25rem; + font-weight: 700; + color: #2d3748; +} + +.chart-select { + padding: 0.5rem 1rem; + border-radius: 8px; + border: 2px solid #e2e8f0; + background: white; + color: #4a5568; + font-weight: 500; + cursor: pointer; +} + +.chart-placeholder { + background: #f7fafc; + border-radius: 12px; + padding: 1.5rem; + min-height: 300px; +} + +.chart-visual { + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.chart-bars { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 200px; + margin-bottom: 1rem; +} + +.chart-bar { + width: 40px; + background: linear-gradient(to top, #667eea 0%, #764ba2 100%); + border-radius: 8px 8px 0 0; + transition: height 0.3s ease; +} + +.chart-bar:hover { + opacity: 0.8; +} + +.chart-labels { + display: flex; + justify-content: space-around; + color: #718096; + font-weight: 500; +} + +.quick-actions { + background: white; + border-radius: 16px; + padding: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.actions-title { + font-size: 1.5rem; + font-weight: 700; + color: #2d3748; + margin-bottom: 1.5rem; + text-align: center; +} + +.actions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.action-btn { + padding: 2rem 1rem; + background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + border: 2px dashed #cbd5e0; + border-radius: 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.action-btn:hover { + background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%); + border-color: #667eea; + transform: translateY(-3px); +} + +.action-icon { + font-size: 2.5rem; +} + +.action-text { + font-size: 1rem; + font-weight: 600; + color: #4a5568; +} + +@media (max-width: 1024px) { + .dashboard-content { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .dashboard-container { + padding: 1rem; + } + + .dashboard-header { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .welcome-title { + font-size: 1.75rem; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .actions-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .actions-grid { + grid-template-columns: 1fr; + } + + .action-btn { + padding: 1.5rem 1rem; + } +} + +@media (prefers-color-scheme: dark) { + .dashboard-container { + color: #f1f5f9; + } + + .actions-title { + color: #f1f5f9 !important; + font-weight: 700; + } + + .chart-title { + color: #f1f5f9 !important; + font-weight: 700; + } + + .dashboard-header { + border-bottom-color: rgba(255, 255, 255, 0.1); + } + + .welcome-title { + background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%); + -webkit-background-clip: text; + background-clip: text; + } + + .welcome-subtitle { + color: #cbd5e1; + } + + .stat-card, + .chart-container, + .quick-actions { + background: rgba(30, 41, 59, 0.95); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); + } + + .stat-value { + color: #f1f5f9; + } + + .stat-title { + color: #cbd5e1; + } + + .progress-bar { + background: #475569; + } + + .chart-placeholder { + background: rgba(30, 41, 59, 0.7); + } + + .action-btn { + background: linear-gradient(135deg, rgba(129, 140, 248, 0.1) 0%, rgba(192, 132, 252, 0.1) 100%); + border-color: #475569; + } + + .action-text { + color: #cbd5e1; + } + + .btn-secondary { + background: rgba(30, 41, 59, 0.9); + color: #e2e8f0; + border-color: #4a5568; + } + + .btn-secondary:hover { + background: rgba(41, 53, 73, 0.95); + border-color: #667eea; + } + + .chart-select { + background: rgba(30, 41, 59, 0.9); + border-color: #4a5568; + color: #cbd5e1; + } +} + +.storage-overview { + background: white; + border-radius: 16px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.section-title { + font-size: 1.5rem; + font-weight: 700; + color: #2d3748; +} + +.storage-summary { + display: flex; + gap: 1.5rem; + font-size: 0.95rem; + color: #718096; +} + +.summary-text strong { + color: #2d3748; + font-weight: 700; +} + +.main-progress-container { + background: #f7fafc; + border-radius: 12px; + padding: 1.5rem; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.progress-label { + font-weight: 600; + color: #4a5568; +} + +.progress-percent { + font-size: 1.25rem; + font-weight: 700; + color: #667eea; +} + +.main-progress-bar { + height: 12px; + background: #e2e8f0; + border-radius: 6px; + overflow: hidden; + margin-bottom: 0.75rem; +} + +.main-progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 6px; + transition: width 0.5s ease; +} + +.progress-details { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + color: #718096; +} + +.detail-item { + font-weight: 500; +} + +.chart-legend { + display: flex; + justify-content: center; + gap: 1.5rem; + margin-top: 1.5rem; + flex-wrap: wrap; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; +} + +.legend-text { + font-size: 0.9rem; + color: #718096; + font-weight: 500; +} + +.quick-access { + background: white; + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.access-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.access-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: #f7fafc; + border-radius: 12px; + transition: background 0.2s ease; +} + +.access-item:hover { + background: #edf2f7; +} + +.access-icon { + font-size: 1.5rem; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: white; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.access-content { + flex: 1; +} + +.access-content h4 { + color: #2d3748; + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.access-content p { + color: #718096; + font-size: 0.9rem; +} + +.access-action { + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.access-action:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.alerts-container { + background: white; + border-radius: 16px; + padding: 2rem; + margin-top: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.alerts-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.alert-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.25rem; + border-radius: 12px; + border-left: 4px solid; +} + +.alert-item.warning { + background: rgba(254, 235, 200, 0.3); + border-left-color: #f6ad55; +} + +.alert-item.info { + background: rgba(190, 227, 248, 0.3); + border-left-color: #4299e1; +} + +.alert-icon { + font-size: 1.5rem; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: white; + border-radius: 10px; +} + +.alert-content { + flex: 1; +} + +.alert-content h4 { + color: #2d3748; + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.alert-content p { + color: #718096; + font-size: 0.9rem; +} + +.alert-action { + padding: 0.5rem 1rem; + background: white; + color: #4a5568; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.alert-action:hover { + background: #f7fafc; + border-color: #cbd5e0; +} + + +@media (prefers-color-scheme: dark) { + .storage-overview, + .quick-access, + .alerts-container { + background: rgba(30, 41, 59, 0.95); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); + } + + .section-title { + color: #f1f5f9; + } + + .storage-summary { + color: #cbd5e1; + } + + .summary-text strong { + color: #f1f5f9; + } + + .main-progress-container { + background: rgba(30, 41, 59, 0.7); + } + + .progress-label { + color: #cbd5e1; + } + + .progress-details { + color: #94a3b8; + } + + .access-item { + background: rgba(30, 41, 59, 0.7); + } + + .access-item:hover { + background: rgba(41, 53, 73, 0.9); + } + + .access-icon { + background: rgba(30, 41, 59, 0.9); + } + + .access-content h4 { + color: #f1f5f9; + } + + .alert-item.warning { + background: rgba(116, 66, 16, 0.4); + border-left-color: #f6ad55; + } + + .alert-item.info { + background: rgba(42, 67, 101, 0.4); + border-left-color: #4299e1; + } + + .alert-icon { + background: rgba(30, 41, 59, 0.9); + color: #cbd5e1; + } + + .alert-content h4 { + color: #f1f5f9 !important; + font-weight: 700; + } + + .alert-content p { + color: #cbd5e1 !important; + opacity: 0.9; + } + + .alert-action { + background: rgba(30, 41, 59, 0.9); + color: #cbd5e1; + border-color: #4a5568; + } + + .alert-action:hover { + background: rgba(41, 53, 73, 0.95); + border-color: #667eea; + } +} + +@media (max-width: 768px) { + .section-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .storage-summary { + flex-direction: column; + gap: 0.5rem; + } + + .access-item { + flex-direction: column; + text-align: center; + } + + .access-content { + text-align: center; + } + + .alert-item { + flex-direction: column; + text-align: center; + } + + .alert-content { + text-align: center; + } +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: none; + justify-content: center; + align-items: center; + z-index: 9999; + backdrop-filter: blur(4px); +} + +.modal { + background: white; + padding: 2rem; + border-radius: 16px; + min-width: 400px; + max-width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + animation: modalSlideIn 0.3s ease-out; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal h3 { + margin-bottom: 1.5rem; + color: #2d3748; + font-size: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: #4a5568; +} + +.form-group input { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1rem; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.modal-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; +} + +@media (prefers-color-scheme: dark) { + .modal { + background: #1a202c; + color: #f7fafc; + } + + .modal h3 { + color: #f7fafc; + } + + .form-group label { + color: #cbd5e0; + } + + .form-group input { + background: #2d3748; + border-color: #4a5568; + color: #f7fafc; + } + + .form-group input:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); + } +} + +.folder-select, .file-input { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1rem; + background: white; +} + +.folder-select:focus, .file-input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.file-preview { + padding: 1rem; + background: #f7fafc; + border-radius: 8px; + border: 2px dashed #cbd5e0; +} + +.file-info { + margin-top: 0.5rem; + font-size: 0.9rem; + color: #718096; +} + +@media (prefers-color-scheme: dark) { + .folder-select, .file-input { + background: #2d3748; + border-color: #4a5568; + color: #f7fafc; + } + + .file-preview { + background: rgba(30, 41, 59, 0.7); + border-color: #4a5568; + } + + .file-info { + color: #cbd5e0; + } +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +/* ========== СТРАНИЦА ПАПКИ ========== */ + +.folder-container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 2rem; + animation: fadeIn 0.6s ease-out; +} + +/* Заголовок папки */ +.folder-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 2px solid rgba(0, 0, 0, 0.05); +} + +.folder-info { + flex: 1; +} + +.folder-title { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 2rem; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + margin-bottom: 0.5rem; +} + +.folder-icon { + font-size: 2.5rem; +} + +.folder-stats { + display: flex; + align-items: center; + gap: 0.75rem; + color: #718096; + font-size: 0.95rem; + flex-wrap: wrap; +} + +.stat-item { + font-weight: 500; +} + +.stat-separator { + color: #cbd5e0; +} + +.folder-actions { + display: flex; + gap: 1rem; +} + +/* Действия с файлами */ +.file-actions-section { + background: white; + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.file-actions-section .section-title { + font-size: 1.5rem; + font-weight: 700; + color: #2d3748; + margin-bottom: 1.25rem; +} + +.action-buttons { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.selection-controls { + margin-left: auto; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + color: #4a5568; + font-weight: 500; + user-select: none; +} + +.checkbox-label input { + display: none; +} + +.checkmark { + width: 18px; + height: 18px; + border: 2px solid #cbd5e0; + border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.checkbox-label input:checked + .checkmark { + background: #667eea; + border-color: #667eea; +} + +.checkbox-label input:checked + .checkmark::after { + content: '✓'; + color: white; + font-size: 12px; + font-weight: bold; +} + +.checkbox-label input:indeterminate + .checkmark { + background: #667eea; + border-color: #667eea; +} + +.checkbox-label input:indeterminate + .checkmark::after { + content: '—'; + color: white; + font-size: 14px; + font-weight: bold; +} + +/* Сетка файлов */ +.files-section { + background: white; + border-radius: 16px; + padding: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.empty-state { + text-align: center; + padding: 3rem 2rem; +} + +.empty-icon { + font-size: 4rem; + margin-bottom: 1.5rem; + opacity: 0.5; +} + +.empty-state h3 { + font-size: 1.5rem; + color: #2d3748; + margin-bottom: 0.5rem; +} + +.empty-state p { + color: #718096; + margin-bottom: 1.5rem; +} + +.files-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.25rem; +} + +.file-card { + background: #f7fafc; + border-radius: 12px; + padding: 1.25rem; + transition: all 0.3s ease; + border: 2px solid transparent; + position: relative; + display: flex; + flex-direction: column; +} + +.file-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + border-color: rgba(102, 126, 234, 0.3); +} + +.file-card.selected { + background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%); + border-color: #667eea; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); +} + +.file-card-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.file-checkbox { + cursor: pointer; + display: flex; + align-items: center; +} + +.file-checkbox input { + display: none; +} + +.file-checkbox .checkmark { + width: 16px; + height: 16px; +} + +.file-icon { + font-size: 2rem; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: white; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.file-info { + flex: 1; +} + +.file-name { + font-size: 1.1rem; + font-weight: 600; + color: #2d3748; + margin-bottom: 0.75rem; + word-break: break-word; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.file-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.85rem; +} + +.detail-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.detail-label { + color: #718096; + font-weight: 500; +} + +.detail-value { + color: #4a5568; + font-weight: 600; +} + +.file-card-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; + justify-content: flex-end; +} + +.file-action-btn { + width: 36px; + height: 36px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + border: none; + cursor: pointer; + transition: all 0.2s ease; + font-size: 1.1rem; +} + +.download-btn { + text-decoration: none !important; + background: none !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + position: relative; + color: white; +} + +.download-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.delete-btn { + background: none !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + position: relative; + background: white; + color: #fc8181; +} + +.delete-btn:hover { + background: #fed7d7; + transform: translateY(-2px); +} + +.delete-form { + display: inline; +} + +.btn-danger { + background: linear-gradient(135deg, #fc8181 0%, #f56565 100%); + color: white; +} + +.btn-danger:hover { + background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(245, 101, 101, 0.3); +} + +.actions-title { + font-size: 1.5rem; + font-weight: 700; + color: #2d3748; + margin-bottom: 1.5rem; + text-align: center; +} + +.actions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.actions-grid .action-btn { + padding: 2rem 1rem; + background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + border: 2px dashed #cbd5e0; + border-radius: 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + cursor: pointer; + transition: all 0.3s ease; + width: 100%; + height: auto; + box-sizing: border-box; +} + +.actions-grid .action-btn:hover { + background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%); + border-color: #667eea; + transform: translateY(-3px); +} + +.actions-grid .action-icon { + font-size: 2.5rem; +} + +.actions-grid .action-text { + font-size: 1rem; + font-weight: 600; + color: #4a5568; +} + +/* Адаптивность */ +@media (max-width: 1024px) { + .files-grid { + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + } +} + +@media (max-width: 768px) { + .folder-container { + padding: 1rem; + } + + .folder-header { + flex-direction: column; + gap: 1rem; + } + + .folder-title { + font-size: 1.75rem; + } + + .folder-stats { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .stat-separator { + display: none; + } + + .action-buttons { + flex-direction: column; + align-items: stretch; + } + + .selection-controls { + margin-left: 0; + margin-top: 0.5rem; + } + + .files-grid { + grid-template-columns: 1fr; + } + + .actions-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .file-card { + padding: 1rem; + } + + .file-card-actions { + flex-direction: column; + } + + .file-action-btn { + width: 100%; + } + + .actions-grid { + grid-template-columns: 1fr; + } + + .actions-grid .action-btn { + padding: 1.5rem 1rem; + } +} + +/* Темная тема */ +@media (prefers-color-scheme: dark) { + .folder-header { + border-bottom-color: rgba(255, 255, 255, 0.1); + } + + .folder-title { + background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%); + -webkit-background-clip: text; + background-clip: text; + } + + .folder-stats { + color: #94a3b8; + } + + .file-actions-section, + .files-section, + .quick-actions { + background: rgba(30, 41, 59, 0.95); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); + } + + .file-actions-section .section-title, + .actions-title { + color: #f1f5f9; + } + + .checkbox-label { + color: #cbd5e1; + } + + .checkmark { + border-color: #475569; + } + + .empty-state h3 { + color: #f1f5f9; + } + + .empty-state p { + color: #94a3b8; + } + + .file-card { + background: rgba(30, 41, 59, 0.7); + border-color: rgba(255, 255, 255, 0.1); + } + + .file-card:hover { + border-color: #818cf8; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); + } + + .file-card.selected { + background: linear-gradient(135deg, rgba(129, 140, 248, 0.2) 0%, rgba(192, 132, 252, 0.2) 100%); + border-color: #818cf8; + } + + .file-icon { + background: rgba(30, 41, 59, 0.9); + color: #cbd5e1; + } + + .file-name { + color: #f1f5f9; + } + + .detail-label { + color: #94a3b8; + } + + .detail-value { + color: #cbd5e1; + } + + .delete-btn { + background: rgba(30, 41, 59, 0.9); + border-color: #4a5568; + color: #feb2b2; + } + + .delete-btn:hover { + background: rgba(254, 178, 178, 0.1); + border-color: #fc8181; + } + + .checkmark { + border-color: #475569; + } + + .checkbox-label input:checked + .checkmark, + .checkbox-label input:indeterminate + .checkmark { + background: #818cf8; + border-color: #818cf8; + } + + /* Темная тема для быстрых действий */ + .quick-actions { + background: rgba(30, 41, 59, 0.95); + } + + .actions-grid .action-btn { + background: linear-gradient(135deg, rgba(129, 140, 248, 0.1) 0%, rgba(192, 132, 252, 0.1) 100%); + border-color: #475569; + } + + .actions-grid .action-btn:hover { + background: linear-gradient(135deg, rgba(129, 140, 248, 0.2) 0%, rgba(192, 132, 252, 0.2) 100%); + border-color: #818cf8; + } + + .actions-grid .action-text { + color: #cbd5e1; + } +} + +.btn-success { + background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); + color: white; +} + +.btn-success:hover { + background: linear-gradient(135deg, #38a169 0%, #2f855a 100%); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3); +} + + diff --git a/public/index.php b/public/index.php new file mode 100755 index 0000000..013bf53 --- /dev/null +++ b/public/index.php @@ -0,0 +1,144 @@ +singleton(StorageDriverInterface::class, function () use ($storageBasePath, $userLimitBytes) { + return new LocalStorageDriver( + basePath: $storageBasePath, + defaultLimitBytes: $userLimitBytes + ); +}); +$container->singleton(UserStorageInitializer::class, function () use ($storageBasePath) { + return new UserStorageInitializer($storageBasePath); +}); +$container->singleton(LoggerInterface::class, function () use ($logPath) { + $logger = new Logger('cloud'); + $logger->pushHandler(new StreamHandler($logPath, Level::Debug)); + $logger->pushProcessor(new PsrLogMessageProcessor()); + return $logger; +}); +$container->singleton(PDO::class, function () { + return new PDO( + 'sqlite:/var/db/database.sqlite', + null, + null, + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] + ); +}); +$container->singleton(Psr17Factory::class, fn() => new Psr17Factory()); + +$container->request( + ServerRequestInterface::class, + fn(Container $c) => new ServerRequestCreator( + $c->get(Psr17Factory::class), + $c->get(Psr17Factory::class), + $c->get(Psr17Factory::class), + $c->get(Psr17Factory::class), + )->fromGlobals() +); + +// --------------------------------------------------------------------- +// Routes +// --------------------------------------------------------------------- +$routes = static function (RouteCollector $r): void { + + $r->get('/', [DashboardController::class, 'index']); + $r->get('/license', [LicenseController::class, 'license']); + + $r->get('/login', [AuthController::class, 'loginForm']); + $r->post('/login', [AuthController::class, 'loginSubmit']); + $r->post('/logout', [AuthController::class, 'logout']); + + $r->post('/storage/folders', [StorageController::class, 'createFolder']); + $r->post('/storage/files', [StorageController::class, 'uploadFile']); + + $r->get('/folders/{folder}', [StorageController::class, 'showFolder']); + + $r->post('/storage/folders/{folder}/delete', [StorageController::class, 'deleteFolder']); + + $r->addRoute(['POST', 'OPTIONS'], '/storage/tus', [ + StorageTusController::class, + 'handle', + ]); + $r->patch('/storage/tus/{id:[a-f0-9]+}', [StorageTusController::class, 'patch']); + $r->head('/storage/tus/{id:[a-f0-9]+}', [StorageTusController::class, 'head']); + + $r->get('/storage/files/download', [StorageController::class, 'downloadFile']); + $r->post('/storage/files/delete', [StorageController::class, 'deleteFile']); + + $r->get('/storage/files/download/multiple', [StorageController::class, 'downloadMultiple']); + $r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']); +}; + + +// route,middlewares +$router = new Router($routes, $container); +//$router->middlewareFor('/', AuthMiddleware::class); +//$router->middlewareFor('/login', AuthMiddleware::class); + + +$app = new App($container); + +//global,middlewares +$app->middleware( + CsrfMiddleware::class, + AuthMiddleware::class +); +$app->router($router); + +try { + $container->beginRequest(); + $app->dispatch(); +} catch (Throwable $e) { + http_response_code(500); + header('Content-Type: text/plain; charset=utf-8'); + echo 'Internal Server Error'; +} \ No newline at end of file diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 0000000..133f33e --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,60 @@ +(function () { + 'use strict'; + + const {modalManager, FileUploader, showNotification} = window.sharedUtils; + + /* ======================= + * ELEMENTS + * ======================= */ + const folderModal = document.getElementById('create-folder-modal'); + const uploadModal = document.getElementById('upload-file-modal'); + + const folderOpenBtn = document.getElementById('create-folder-btn'); + const uploadOpenBtn = document.getElementById('upload-file-btn'); + + const folderCloseBtn = document.getElementById('cancel-create-folder'); + const uploadCloseBtn = document.getElementById('cancel-upload-file'); + + document.addEventListener('DOMContentLoaded', () => { + if (folderModal) folderModal.style.display = 'none'; + if (uploadModal) uploadModal.style.display = 'none'; + + if (uploadModal) { + new FileUploader({ + formSelector: '#upload-file-modal form', + fileInputSelector: '#upload-file-modal input[type="file"]', + folderSelectSelector: '#upload-file-modal select[name="folder"]', + progressFillSelector: '#upload-file-modal .progress-fill', + progressTextSelector: '#upload-file-modal .progress-text', + uploadProgressSelector: '#upload-file-modal .upload-progress', + submitBtnSelector: '#upload-file-modal #submit-upload', + cancelBtnSelector: '#upload-file-modal #cancel-upload-file' + }); + } + + if (folderOpenBtn) { + folderOpenBtn.addEventListener('click', () => modalManager.open(folderModal)); + } + + if (uploadOpenBtn) { + uploadOpenBtn.addEventListener('click', () => modalManager.open(uploadModal)); + } + + if (folderCloseBtn) { + folderCloseBtn.addEventListener('click', () => modalManager.close()); + } + + if (uploadCloseBtn) { + uploadCloseBtn.addEventListener('click', () => modalManager.close()); + } + + [folderModal, uploadModal].forEach(modal => { + if (modal) { + modal.addEventListener('click', e => { + if (e.target === modal) modalManager.close(); + }); + } + }); + }); + +})(); \ No newline at end of file diff --git a/public/js/folder.js b/public/js/folder.js new file mode 100644 index 0000000..aba87c3 --- /dev/null +++ b/public/js/folder.js @@ -0,0 +1,229 @@ +(function () { + 'use strict'; + + const {modalManager, FileUploader, showNotification} = window.sharedUtils; + + /* ======================= + * SELECT/DELETE FILES LOGIC + * ======================= */ + class FileSelectionManager { + constructor() { + this.selectAllCheckbox = document.getElementById('select-all-checkbox'); + this.fileCheckboxes = document.querySelectorAll('.file-select-checkbox'); + this.fileCards = document.querySelectorAll('.file-card'); + this.deleteMultipleForm = document.querySelector('.multiple-delete-form'); + this.downloadMultipleForm = document.querySelector('.multiple-download-form'); + this.deleteMultipleBtn = document.getElementById('delete-multiple-btn'); + this.downloadMultipleBtn = document.getElementById('download-multiple-btn'); + this.multipleFileNamesInput = document.getElementById('multiple-file-names'); + this.multipleDownloadNamesInput = document.getElementById('multiple-download-names'); + + this.init(); + } + + init() { + if (this.selectAllCheckbox) { + this.selectAllCheckbox.addEventListener('change', () => this.toggleSelectAll()); + } + + this.fileCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => this.updateSelectionState()); + }); + + this.fileCards.forEach(card => { + card.addEventListener('click', (e) => this.handleCardClick(e, card)); + }); + + if (this.deleteMultipleForm) { + this.deleteMultipleForm.addEventListener('submit', (e) => this.handleMultipleDelete(e)); + } + + if (this.downloadMultipleForm) { + this.downloadMultipleForm.addEventListener('submit', (e) => this.handleMultipleDownload(e)); + } + } + + handleCardClick(e, card) { + if (e.target.closest('.file-card-actions') || + e.target.closest('.file-action-btn') || + e.target.closest('.delete-form') || + e.target.closest('a')) { + return; + } + + const checkbox = card.querySelector('.file-select-checkbox'); + if (checkbox) { + checkbox.checked = !checkbox.checked; + checkbox.dispatchEvent(new Event('change')); + } + } + + toggleSelectAll() { + const isChecked = this.selectAllCheckbox.checked; + this.fileCheckboxes.forEach(checkbox => { + checkbox.checked = isChecked; + checkbox.dispatchEvent(new Event('change')); + }); + this.updateSelectionState(); + } + + updateSelectionState() { + const selectedFiles = Array.from(this.fileCheckboxes) + .filter(cb => cb.checked) + .map(cb => cb.value); + + // flash selected + this.fileCards.forEach(card => { + const fileName = card.dataset.fileName; + if (selectedFiles.includes(fileName)) { + card.classList.add('selected'); + } else { + card.classList.remove('selected'); + } + }); + + // show/hide action-buttons + if (selectedFiles.length > 0) { + this.deleteMultipleForm.style.display = 'block'; + this.downloadMultipleForm.style.display = 'inline-block'; + this.multipleFileNamesInput.value = JSON.stringify(selectedFiles); + this.multipleDownloadNamesInput.value = JSON.stringify(selectedFiles); + + this.deleteMultipleBtn.innerHTML = ` + 🗑️ + Delete Selected (${selectedFiles.length}) + `; + this.downloadMultipleBtn.innerHTML = ` + ⬇️ + Download Selected (${selectedFiles.length}) + `; + } else { + this.deleteMultipleForm.style.display = 'none'; + this.downloadMultipleForm.style.display = 'none'; + } + + // refresh select all + if (this.selectAllCheckbox) { + this.selectAllCheckbox.checked = selectedFiles.length === this.fileCheckboxes.length && this.fileCheckboxes.length > 0; + this.selectAllCheckbox.indeterminate = selectedFiles.length > 0 && selectedFiles.length < this.fileCheckboxes.length; + } + } + + handleMultipleDelete(e) { + if (!confirm('Are you sure you want to delete selected files?')) { + e.preventDefault(); + return false; + } + return true; + } + + handleMultipleDownload(e) { + console.log('Downloading selected files...'); + return true; + } + } + + /* ======================= + * FILE PREVIEW + * ======================= */ + class FilePreviewManager { + constructor(fileInput, previewContainer) { + this.fileInput = fileInput; + this.previewContainer = previewContainer; + this.img = previewContainer.querySelector('img'); + this.info = previewContainer.querySelector('.file-info'); + + this.init(); + } + + init() { + this.fileInput.addEventListener('change', () => this.updatePreview()); + } + + updatePreview() { + const file = this.fileInput.files[0]; + + if (!file) { + this.previewContainer.style.display = 'none'; + return; + } + + this.previewContainer.style.display = 'block'; + this.info.innerHTML = ` +
Name: ${file.name}
+
Size: ${formatBytes(file.size)}
+
Type: ${file.type || 'Unknown'}
+ `; + + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = (e) => { + this.img.src = e.target.result; + this.img.style.display = 'block'; + }; + reader.readAsDataURL(file); + } else { + this.img.style.display = 'none'; + } + } + + reset() { + this.previewContainer.style.display = 'none'; + this.img.src = ''; + this.img.style.display = 'none'; + this.info.innerHTML = ''; + } + } + + /* ======================= + * Load init + * ======================= */ + document.addEventListener('DOMContentLoaded', () => { + const uploadModal = document.getElementById('upload-file-modal-folder'); + if (uploadModal) { + uploadModal.style.display = 'none'; + + new FileUploader({ + formSelector: '#upload-form-folder', + fileInputSelector: '#file-input-folder', + progressFillSelector: '#upload-file-modal-folder .progress-fill', + progressTextSelector: '#upload-file-modal-folder .progress-text', + uploadProgressSelector: '#upload-file-modal-folder .upload-progress', + submitBtnSelector: '#upload-file-modal-folder #submit-upload-folder', + cancelBtnSelector: '#upload-file-modal-folder #cancel-upload-file-folder', + onSuccess: () => { + showNotification('File uploaded successfully', 'success'); + setTimeout(() => location.reload(), 1000); + } + }); + + const fileInput = document.getElementById('file-input-folder'); + const previewContainer = uploadModal.querySelector('.file-preview'); + if (fileInput && previewContainer) { + new FilePreviewManager(fileInput, previewContainer); + } + } + + if (document.querySelector('.file-select-checkbox')) { + new FileSelectionManager(); + } + + const uploadBtn = document.getElementById('upload-file-folder'); + const uploadFirstBtn = document.getElementById('upload-first-file'); + const cancelUploadBtn = document.getElementById('cancel-upload-file-folder'); + + const openUploadModal = () => modalManager.open(uploadModal); + const closeUploadModal = () => modalManager.close(); + + if (uploadBtn) uploadBtn.addEventListener('click', openUploadModal); + if (uploadFirstBtn) uploadFirstBtn.addEventListener('click', openUploadModal); + if (cancelUploadBtn) cancelUploadBtn.addEventListener('click', closeUploadModal); + + if (uploadModal) { + uploadModal.addEventListener('click', e => { + if (e.target === uploadModal) closeUploadModal(); + }); + } + }); + +})(); \ No newline at end of file diff --git a/public/js/shared.js b/public/js/shared.js new file mode 100644 index 0000000..290f99f --- /dev/null +++ b/public/js/shared.js @@ -0,0 +1,363 @@ +// /js/shared.js +(function () { + 'use strict'; + + /* ======================= + * CONFIG + * ======================= */ + const SMALL_FILE_LIMIT = 256 * 1024 * 1024; // 256MB + const TUS_ENDPOINT = '/storage/tus'; + + /* ======================= + * STATE + * ======================= */ + let currentUploadType = ''; + + /* ======================= + * UTILS + * ======================= */ + function formatBytes(bytes) { + if (!bytes) return '0 Bytes'; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; + } + + function getErrorMessage(code) { + const errors = { + no_file: 'No file selected', + upload_failed: 'Upload failed', + storage_limit: 'Storage limit exceeded', + invalid_folder: 'Invalid folder selected' + }; + return errors[code] || 'Unknown error'; + } + + function getSuccessMessage(code) { + const success = { + file_uploaded: 'File uploaded successfully', + folder_created: 'Folder created successfully', + files_deleted: 'Files deleted successfully' + }; + return success[code] || 'Success'; + } + + /* ======================= + * NOTIFICATIONS + * ======================= */ + window.showNotification = function (message, type = 'info') { + const body = document.body; + const notification = document.createElement('div'); + + const oldNotifications = document.querySelectorAll('.global-notification'); + oldNotifications.forEach(n => n.remove()); + + notification.className = 'global-notification'; + notification.textContent = message; + + const bgColor = type === 'error' ? '#e53e3e' : + type === 'success' ? '#38a169' : + type === 'warning' ? '#d69e2e' : '#3182ce'; + + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 16px; + background: ${bgColor}; + color: #fff; + border-radius: 8px; + z-index: 10000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + animation: slideIn 0.3s ease-out; + `; + + body.appendChild(notification); + + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease-in'; + setTimeout(() => notification.remove(), 300); + }, 3000); + + if (!document.querySelector('#notification-styles')) { + const style = document.createElement('style'); + style.id = 'notification-styles'; + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } + `; + document.head.appendChild(style); + } + }; + + /* ======================= + * MODAL MANAGEMENT + * ======================= */ + window.modalManager = { + currentModal: null, + + open(modal) { + this.closeAll(); + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + this.currentModal = modal; + + const escHandler = (e) => { + if (e.key === 'Escape') this.close(); + }; + modal._escHandler = escHandler; + document.addEventListener('keydown', escHandler); + }, + + close() { + if (this.currentModal) { + document.removeEventListener('keydown', this.currentModal._escHandler); + this.currentModal.style.display = 'none'; + document.body.style.overflow = ''; + this.currentModal = null; + } + }, + + closeAll() { + document.querySelectorAll('.modal-overlay').forEach(modal => { + modal.style.display = 'none'; + if (modal._escHandler) { + document.removeEventListener('keydown', modal._escHandler); + } + }); + document.body.style.overflow = ''; + this.currentModal = null; + } + }; + + /* ======================= + * FILE UPLOAD + * ======================= */ + class FileUploader { + constructor(options = {}) { + this.options = { + formSelector: '', + fileInputSelector: '', + folderSelectSelector: null, + progressFillSelector: '.progress-fill', + progressTextSelector: '.progress-text', + uploadProgressSelector: '.upload-progress', + submitBtnSelector: '', + cancelBtnSelector: '', + onSuccess: null, + onError: null, + ...options + }; + + this.form = document.querySelector(this.options.formSelector); + this.fileInput = document.querySelector(this.options.fileInputSelector); + this.folderSelect = this.options.folderSelectSelector ? + document.querySelector(this.options.folderSelectSelector) : null; + this.progressFill = document.querySelector(this.options.progressFillSelector); + this.progressText = document.querySelector(this.options.progressTextSelector); + this.uploadProgress = document.querySelector(this.options.uploadProgressSelector); + this.submitBtn = document.querySelector(this.options.submitBtnSelector); + this.cancelBtn = document.querySelector(this.options.cancelBtnSelector); + + this.bindEvents(); + } + + bindEvents() { + if (this.form) { + this.form.addEventListener('submit', (e) => this.handleSubmit(e)); + } + + if (this.fileInput) { + this.fileInput.addEventListener('change', () => this.handleFileSelect()); + } + + if (this.cancelBtn) { + this.cancelBtn.addEventListener('click', () => this.reset()); + } + } + + handleFileSelect() { + const file = this.fileInput.files[0]; + if (!file) return; + + console.log('File selected:', file.name, formatBytes(file.size)); + } + + async handleSubmit(e) { + e.preventDefault(); + + const file = this.fileInput.files[0]; + const folder = this.folderSelect ? this.folderSelect.value : this.form.querySelector('input[name="folder"]')?.value; + + if (!file) { + showNotification('Please select a file', 'error'); + return; + } + + if (this.folderSelect && !folder) { + showNotification('Please select a folder', 'error'); + return; + } + + this.setUploadingState(true); + + if (file.size <= SMALL_FILE_LIMIT) { + await this.uploadSmallFile(file, folder); + } else { + this.uploadLargeFile(file, folder); + } + } + + async uploadSmallFile(file, folder) { + try { + this.updateProgress(0, 'Starting upload...'); + + const formData = new FormData(); + formData.append('file', file); + if (folder) formData.append('folder', folder); + formData.append('_csrf', this.form.querySelector('input[name="_csrf"]')?.value || ''); + + let progress = 0; + const progressInterval = setInterval(() => { + if (progress < 90) { + progress += 10; + this.updateProgress(progress); + } + }, 300); + + const response = await fetch('/storage/files', { + method: 'POST', + body: formData + }); + + clearInterval(progressInterval); + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'Upload failed'); + } + + this.updateProgress(100, 'Upload complete!'); + this.onUploadSuccess(); + + } catch (error) { + this.onUploadError(error.message); + } + } + + uploadLargeFile(file, folder) { + this.updateProgress(0, 'Starting large file upload...'); + + const upload = new tus.Upload(file, { + endpoint: TUS_ENDPOINT, + chunkSize: 5 * 1024 * 1024, + retryDelays: [0, 1000, 3000, 5000], + metadata: { + folder: folder || '', + filename: file.name + }, + withCredentials: true, + + onProgress: (uploaded, total) => { + const percent = Math.round((uploaded / total) * 100); + this.updateProgress(percent, `${percent}%`); + }, + + onSuccess: () => { + this.updateProgress(100, 'Upload complete!'); + setTimeout(() => this.onUploadSuccess(), 500); + }, + + onError: (error) => { + this.onUploadError(error.toString()); + } + }); + + upload.start(); + } + + updateProgress(percent, text = '') { + if (this.progressFill) { + this.progressFill.style.width = percent + '%'; + } + if (this.progressText) { + this.progressText.textContent = text || percent + '%'; + } + } + + onUploadSuccess() { + showNotification('File uploaded successfully', 'success'); + this.setUploadingState(false); + modalManager.close(); + this.reset(); + + if (this.options.onSuccess) { + this.options.onSuccess(); + } else { + setTimeout(() => location.reload(), 1000); + } + } + + onUploadError(error) { + showNotification(error, 'error'); + this.setUploadingState(false); + this.reset(); + + if (this.options.onError) { + this.options.onError(error); + } + } + + setUploadingState(uploading) { + if (this.submitBtn) this.submitBtn.disabled = uploading; + if (this.cancelBtn) this.cancelBtn.disabled = uploading; + if (this.uploadProgress) { + this.uploadProgress.style.display = uploading ? 'block' : 'none'; + } + } + + reset() { + if (this.form) this.form.reset(); + this.updateProgress(0); + this.setUploadingState(false); + } + } + + /* ======================= + * INIT URL PARAMS + * ======================= */ + function initUrlParams() { + const params = new URLSearchParams(window.location.search); + if (params.get('error')) { + showNotification(getErrorMessage(params.get('error')), 'error'); + } + if (params.get('success')) { + showNotification(getSuccessMessage(params.get('success')), 'success'); + } + } + + /* ======================= + * EXPORTS + * ======================= */ + window.sharedUtils = { + formatBytes, + getErrorMessage, + getSuccessMessage, + showNotification, + modalManager, + FileUploader, + initUrlParams, + SMALL_FILE_LIMIT, + TUS_ENDPOINT + }; + + document.addEventListener('DOMContentLoaded', initUrlParams); + +})(); \ No newline at end of file diff --git a/public/js/tus.js b/public/js/tus.js new file mode 100644 index 0000000..0cca200 --- /dev/null +++ b/public/js/tus.js @@ -0,0 +1,4989 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.tus = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i= 0; --o) { var i = this.tryEntries[o], a = i.completion; if ("root" === i.tryLoc) return handle("end"); if (i.tryLoc <= this.prev) { var c = n.call(i, "catchLoc"), u = n.call(i, "finallyLoc"); if (c && u) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } else if (c) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); } else { if (!u) throw Error("try statement without catch or finally"); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } } } }, abrupt: function abrupt(t, e) { for (var r = this.tryEntries.length - 1; r >= 0; --r) { var o = this.tryEntries[r]; if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) { var i = o; break; } } i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null); var a = i ? i.completion : {}; return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a); }, complete: function complete(t, e) { if ("throw" === t.type) throw t.arg; return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y; }, finish: function finish(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y; } }, "catch": function _catch(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.tryLoc === t) { var n = r.completion; if ("throw" === n.type) { var o = n.arg; resetTryEntry(r); } return o; } } throw Error("illegal catch attempt"); }, delegateYield: function delegateYield(e, r, n) { return this.delegate = { iterator: values(e), resultName: r, nextLoc: n }, "next" === this.method && (this.arg = t), y; } }, e; } +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var FileReader = exports["default"] = /*#__PURE__*/function () { + function FileReader() { + _classCallCheck(this, FileReader); + } + return _createClass(FileReader, [{ + key: "openFile", + value: function () { + var _openFile = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(input, chunkSize) { + var blob; + return _regeneratorRuntime().wrap(function _callee$(_context) { + while (1) switch (_context.prev = _context.next) { + case 0: + if (!((0, _isReactNative["default"])() && input && typeof input.uri !== 'undefined')) { + _context.next = 11; + break; + } + _context.prev = 1; + _context.next = 4; + return (0, _uriToBlob["default"])(input.uri); + case 4: + blob = _context.sent; + return _context.abrupt("return", new _FileSource["default"](blob)); + case 8: + _context.prev = 8; + _context.t0 = _context["catch"](1); + throw new Error("tus: cannot fetch `file.uri` as Blob, make sure the uri is correct and accessible. ".concat(_context.t0)); + case 11: + if (!(typeof input.slice === 'function' && typeof input.size !== 'undefined')) { + _context.next = 13; + break; + } + return _context.abrupt("return", Promise.resolve(new _FileSource["default"](input))); + case 13: + if (!(typeof input.read === 'function')) { + _context.next = 18; + break; + } + chunkSize = Number(chunkSize); + if (Number.isFinite(chunkSize)) { + _context.next = 17; + break; + } + return _context.abrupt("return", Promise.reject(new Error('cannot create source for stream without a finite value for the `chunkSize` option'))); + case 17: + return _context.abrupt("return", Promise.resolve(new _StreamSource["default"](input, chunkSize))); + case 18: + return _context.abrupt("return", Promise.reject(new Error('source object may only be an instance of File, Blob, or Reader in this environment'))); + case 19: + case "end": + return _context.stop(); + } + }, _callee, null, [[1, 8]]); + })); + function openFile(_x, _x2) { + return _openFile.apply(this, arguments); + } + return openFile; + }() + }]); +}(); + +},{"./isReactNative.js":5,"./sources/FileSource.js":6,"./sources/StreamSource.js":7,"./uriToBlob.js":10}],2:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = fingerprint; +var _isReactNative = _interopRequireDefault(require("./isReactNative.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +// TODO: Differenciate between input types + +/** + * Generate a fingerprint for a file which will be used the store the endpoint + * + * @param {File} file + * @param {Object} options + * @param {Function} callback + */ +function fingerprint(file, options) { + if ((0, _isReactNative["default"])()) { + return Promise.resolve(reactNativeFingerprint(file, options)); + } + return Promise.resolve(['tus-br', file.name, file.type, file.size, file.lastModified, options.endpoint].join('-')); +} +function reactNativeFingerprint(file, options) { + var exifHash = file.exif ? hashCode(JSON.stringify(file.exif)) : 'noexif'; + return ['tus-rn', file.name || 'noname', file.size || 'nosize', exifHash, options.endpoint].join('/'); +} +function hashCode(str) { + // from https://stackoverflow.com/a/8831937/151666 + var hash = 0; + if (str.length === 0) { + return hash; + } + for (var i = 0; i < str.length; i++) { + var _char = str.charCodeAt(i); + hash = (hash << 5) - hash + _char; + hash &= hash; // Convert to 32bit integer + } + return hash; +} + +},{"./isReactNative.js":5}],3:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var XHRHttpStack = exports["default"] = /*#__PURE__*/function () { + function XHRHttpStack() { + _classCallCheck(this, XHRHttpStack); + } + return _createClass(XHRHttpStack, [{ + key: "createRequest", + value: function createRequest(method, url) { + return new Request(method, url); + } + }, { + key: "getName", + value: function getName() { + return 'XHRHttpStack'; + } + }]); +}(); +var Request = /*#__PURE__*/function () { + function Request(method, url) { + _classCallCheck(this, Request); + this._xhr = new XMLHttpRequest(); + this._xhr.open(method, url, true); + this._method = method; + this._url = url; + this._headers = {}; + } + return _createClass(Request, [{ + key: "getMethod", + value: function getMethod() { + return this._method; + } + }, { + key: "getURL", + value: function getURL() { + return this._url; + } + }, { + key: "setHeader", + value: function setHeader(header, value) { + this._xhr.setRequestHeader(header, value); + this._headers[header] = value; + } + }, { + key: "getHeader", + value: function getHeader(header) { + return this._headers[header]; + } + }, { + key: "setProgressHandler", + value: function setProgressHandler(progressHandler) { + // Test support for progress events before attaching an event listener + if (!('upload' in this._xhr)) { + return; + } + this._xhr.upload.onprogress = function (e) { + if (!e.lengthComputable) { + return; + } + progressHandler(e.loaded); + }; + } + }, { + key: "send", + value: function send() { + var _this = this; + var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + return new Promise(function (resolve, reject) { + _this._xhr.onload = function () { + resolve(new Response(_this._xhr)); + }; + _this._xhr.onerror = function (err) { + reject(err); + }; + _this._xhr.send(body); + }); + } + }, { + key: "abort", + value: function abort() { + this._xhr.abort(); + return Promise.resolve(); + } + }, { + key: "getUnderlyingObject", + value: function getUnderlyingObject() { + return this._xhr; + } + }]); +}(); +var Response = /*#__PURE__*/function () { + function Response(xhr) { + _classCallCheck(this, Response); + this._xhr = xhr; + } + return _createClass(Response, [{ + key: "getStatus", + value: function getStatus() { + return this._xhr.status; + } + }, { + key: "getHeader", + value: function getHeader(header) { + return this._xhr.getResponseHeader(header); + } + }, { + key: "getBody", + value: function getBody() { + return this._xhr.responseText; + } + }, { + key: "getUnderlyingObject", + value: function getUnderlyingObject() { + return this._xhr; + } + }]); +}(); + +},{}],4:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "DefaultHttpStack", { + enumerable: true, + get: function get() { + return _httpStack["default"]; + } +}); +Object.defineProperty(exports, "DetailedError", { + enumerable: true, + get: function get() { + return _error["default"]; + } +}); +exports.Upload = void 0; +Object.defineProperty(exports, "canStoreURLs", { + enumerable: true, + get: function get() { + return _urlStorage.canStoreURLs; + } +}); +exports.defaultOptions = void 0; +Object.defineProperty(exports, "enableDebugLog", { + enumerable: true, + get: function get() { + return _logger.enableDebugLog; + } +}); +exports.isSupported = void 0; +var _error = _interopRequireDefault(require("../error.js")); +var _logger = require("../logger.js"); +var _noopUrlStorage = _interopRequireDefault(require("../noopUrlStorage.js")); +var _upload = _interopRequireDefault(require("../upload.js")); +var _fileReader = _interopRequireDefault(require("./fileReader.js")); +var _fileSignature = _interopRequireDefault(require("./fileSignature.js")); +var _httpStack = _interopRequireDefault(require("./httpStack.js")); +var _urlStorage = require("./urlStorage.js"); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } +function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } +function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } +function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var defaultOptions = exports.defaultOptions = _objectSpread(_objectSpread({}, _upload["default"].defaultOptions), {}, { + httpStack: new _httpStack["default"](), + fileReader: new _fileReader["default"](), + urlStorage: _urlStorage.canStoreURLs ? new _urlStorage.WebStorageUrlStorage() : new _noopUrlStorage["default"](), + fingerprint: _fileSignature["default"] +}); +var Upload = exports.Upload = /*#__PURE__*/function (_BaseUpload) { + function Upload() { + var file = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + _classCallCheck(this, Upload); + options = _objectSpread(_objectSpread({}, defaultOptions), options); + return _callSuper(this, Upload, [file, options]); + } + _inherits(Upload, _BaseUpload); + return _createClass(Upload, null, [{ + key: "terminate", + value: function terminate(url) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + options = _objectSpread(_objectSpread({}, defaultOptions), options); + return _upload["default"].terminate(url, options); + } + }]); +}(_upload["default"]); // Note: We don't reference `window` here because these classes also exist in a Web Worker's context. +var isSupported = exports.isSupported = typeof XMLHttpRequest === 'function' && typeof Blob === 'function' && typeof Blob.prototype.slice === 'function'; + +},{"../error.js":12,"../logger.js":13,"../noopUrlStorage.js":14,"../upload.js":15,"./fileReader.js":1,"./fileSignature.js":2,"./httpStack.js":3,"./urlStorage.js":11}],5:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +var isReactNative = function isReactNative() { + return typeof navigator !== 'undefined' && typeof navigator.product === 'string' && navigator.product.toLowerCase() === 'reactnative'; +}; +var _default = exports["default"] = isReactNative; + +},{}],6:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +var _isCordova = _interopRequireDefault(require("./isCordova.js")); +var _readAsByteArray = _interopRequireDefault(require("./readAsByteArray.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var FileSource = exports["default"] = /*#__PURE__*/function () { + // Make this.size a method + function FileSource(file) { + _classCallCheck(this, FileSource); + this._file = file; + this.size = file.size; + } + return _createClass(FileSource, [{ + key: "slice", + value: function slice(start, end) { + // In Apache Cordova applications, a File must be resolved using + // FileReader instances, see + // https://cordova.apache.org/docs/en/8.x/reference/cordova-plugin-file/index.html#read-a-file + if ((0, _isCordova["default"])()) { + return (0, _readAsByteArray["default"])(this._file.slice(start, end)); + } + var value = this._file.slice(start, end); + var done = end >= this.size; + return Promise.resolve({ + value: value, + done: done + }); + } + }, { + key: "close", + value: function close() { + // Nothing to do here since we don't need to release any resources. + } + }]); +}(); + +},{"./isCordova.js":8,"./readAsByteArray.js":9}],7:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +function len(blobOrArray) { + if (blobOrArray === undefined) return 0; + if (blobOrArray.size !== undefined) return blobOrArray.size; + return blobOrArray.length; +} + +/* + Typed arrays and blobs don't have a concat method. + This function helps StreamSource accumulate data to reach chunkSize. +*/ +function concat(a, b) { + if (a.concat) { + // Is `a` an Array? + return a.concat(b); + } + if (a instanceof Blob) { + return new Blob([a, b], { + type: a.type + }); + } + if (a.set) { + // Is `a` a typed array? + var c = new a.constructor(a.length + b.length); + c.set(a); + c.set(b, a.length); + return c; + } + throw new Error('Unknown data type'); +} +var StreamSource = exports["default"] = /*#__PURE__*/function () { + function StreamSource(reader) { + _classCallCheck(this, StreamSource); + this._buffer = undefined; + this._bufferOffset = 0; + this._reader = reader; + this._done = false; + } + return _createClass(StreamSource, [{ + key: "slice", + value: function slice(start, end) { + if (start < this._bufferOffset) { + return Promise.reject(new Error("Requested data is before the reader's current offset")); + } + return this._readUntilEnoughDataOrDone(start, end); + } + }, { + key: "_readUntilEnoughDataOrDone", + value: function _readUntilEnoughDataOrDone(start, end) { + var _this = this; + var hasEnoughData = end <= this._bufferOffset + len(this._buffer); + if (this._done || hasEnoughData) { + var value = this._getDataFromBuffer(start, end); + var done = value == null ? this._done : false; + return Promise.resolve({ + value: value, + done: done + }); + } + return this._reader.read().then(function (_ref) { + var value = _ref.value, + done = _ref.done; + if (done) { + _this._done = true; + } else if (_this._buffer === undefined) { + _this._buffer = value; + } else { + _this._buffer = concat(_this._buffer, value); + } + return _this._readUntilEnoughDataOrDone(start, end); + }); + } + }, { + key: "_getDataFromBuffer", + value: function _getDataFromBuffer(start, end) { + // Remove data from buffer before `start`. + // Data might be reread from the buffer if an upload fails, so we can only + // safely delete data when it comes *before* what is currently being read. + if (start > this._bufferOffset) { + this._buffer = this._buffer.slice(start - this._bufferOffset); + this._bufferOffset = start; + } + // If the buffer is empty after removing old data, all data has been read. + var hasAllDataBeenRead = len(this._buffer) === 0; + if (this._done && hasAllDataBeenRead) { + return null; + } + // We already removed data before `start`, so we just return the first + // chunk from the buffer. + return this._buffer.slice(0, end - start); + } + }, { + key: "close", + value: function close() { + if (this._reader.cancel) { + this._reader.cancel(); + } + } + }]); +}(); + +},{}],8:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +var isCordova = function isCordova() { + return typeof window !== 'undefined' && (typeof window.PhoneGap !== 'undefined' || typeof window.Cordova !== 'undefined' || typeof window.cordova !== 'undefined'); +}; +var _default = exports["default"] = isCordova; + +},{}],9:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = readAsByteArray; +/** + * readAsByteArray converts a File object to a Uint8Array. + * This function is only used on the Apache Cordova platform. + * See https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-file/index.html#read-a-file + */ +function readAsByteArray(chunk) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function () { + var value = new Uint8Array(reader.result); + resolve({ + value: value + }); + }; + reader.onerror = function (err) { + reject(err); + }; + reader.readAsArrayBuffer(chunk); + }); +} + +},{}],10:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = uriToBlob; +/** + * uriToBlob resolves a URI to a Blob object. This is used for + * React Native to retrieve a file (identified by a file:// + * URI) as a blob. + */ +function uriToBlob(uri) { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.responseType = 'blob'; + xhr.onload = function () { + var blob = xhr.response; + resolve(blob); + }; + xhr.onerror = function (err) { + reject(err); + }; + xhr.open('GET', uri); + xhr.send(); + }); +} + +},{}],11:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.canStoreURLs = exports.WebStorageUrlStorage = void 0; +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var hasStorage = false; +try { + // Note: localStorage does not exist in the Web Worker's context, so we must use window here. + hasStorage = 'localStorage' in window; + + // Attempt to store and read entries from the local storage to detect Private + // Mode on Safari on iOS (see #49) + // If the key was not used before, we remove it from local storage again to + // not cause confusion where the entry came from. + var key = 'tusSupport'; + var originalValue = localStorage.getItem(key); + localStorage.setItem(key, originalValue); + if (originalValue === null) localStorage.removeItem(key); +} catch (e) { + // If we try to access localStorage inside a sandboxed iframe, a SecurityError + // is thrown. When in private mode on iOS Safari, a QuotaExceededError is + // thrown (see #49) + if (e.code === e.SECURITY_ERR || e.code === e.QUOTA_EXCEEDED_ERR) { + hasStorage = false; + } else { + throw e; + } +} +var canStoreURLs = exports.canStoreURLs = hasStorage; +var WebStorageUrlStorage = exports.WebStorageUrlStorage = /*#__PURE__*/function () { + function WebStorageUrlStorage() { + _classCallCheck(this, WebStorageUrlStorage); + } + return _createClass(WebStorageUrlStorage, [{ + key: "findAllUploads", + value: function findAllUploads() { + var results = this._findEntries('tus::'); + return Promise.resolve(results); + } + }, { + key: "findUploadsByFingerprint", + value: function findUploadsByFingerprint(fingerprint) { + var results = this._findEntries("tus::".concat(fingerprint, "::")); + return Promise.resolve(results); + } + }, { + key: "removeUpload", + value: function removeUpload(urlStorageKey) { + localStorage.removeItem(urlStorageKey); + return Promise.resolve(); + } + }, { + key: "addUpload", + value: function addUpload(fingerprint, upload) { + var id = Math.round(Math.random() * 1e12); + var key = "tus::".concat(fingerprint, "::").concat(id); + localStorage.setItem(key, JSON.stringify(upload)); + return Promise.resolve(key); + } + }, { + key: "_findEntries", + value: function _findEntries(prefix) { + var results = []; + for (var i = 0; i < localStorage.length; i++) { + var _key = localStorage.key(i); + if (_key.indexOf(prefix) !== 0) continue; + try { + var upload = JSON.parse(localStorage.getItem(_key)); + upload.urlStorageKey = _key; + results.push(upload); + } catch (_e) { + // The JSON parse error is intentionally ignored here, so a malformed + // entry in the storage cannot prevent an upload. + } + } + return results; + } + }]); +}(); + +},{}],12:[function(require,module,exports){ +"use strict"; + +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } +function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } +function _construct(t, e, r) { if (_isNativeReflectConstruct()) return Reflect.construct.apply(null, arguments); var o = [null]; o.push.apply(o, e); var p = new (t.bind.apply(t, o))(); return r && _setPrototypeOf(p, r.prototype), p; } +function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } +function _isNativeFunction(fn) { try { return Function.toString.call(fn).indexOf("[native code]") !== -1; } catch (e) { return typeof fn === "function"; } } +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } +var DetailedError = /*#__PURE__*/function (_Error) { + function DetailedError(message) { + var _this; + var causingErr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + var req = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + var res = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + _classCallCheck(this, DetailedError); + _this = _callSuper(this, DetailedError, [message]); + _this.originalRequest = req; + _this.originalResponse = res; + _this.causingError = causingErr; + if (causingErr != null) { + message += ", caused by ".concat(causingErr.toString()); + } + if (req != null) { + var requestId = req.getHeader('X-Request-ID') || 'n/a'; + var method = req.getMethod(); + var url = req.getURL(); + var status = res ? res.getStatus() : 'n/a'; + var body = res ? res.getBody() || '' : 'n/a'; + message += ", originated from request (method: ".concat(method, ", url: ").concat(url, ", response code: ").concat(status, ", response text: ").concat(body, ", request id: ").concat(requestId, ")"); + } + _this.message = message; + return _this; + } + _inherits(DetailedError, _Error); + return _createClass(DetailedError); +}( /*#__PURE__*/_wrapNativeSuper(Error)); +var _default = exports["default"] = DetailedError; + +},{}],13:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.enableDebugLog = enableDebugLog; +exports.log = log; +var isEnabled = false; +function enableDebugLog() { + isEnabled = true; +} +function log(msg) { + if (!isEnabled) return; + console.log(msg); +} + +},{}],14:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var NoopUrlStorage = exports["default"] = /*#__PURE__*/function () { + function NoopUrlStorage() { + _classCallCheck(this, NoopUrlStorage); + } + return _createClass(NoopUrlStorage, [{ + key: "listAllUploads", + value: function listAllUploads() { + return Promise.resolve([]); + } + }, { + key: "findUploadsByFingerprint", + value: function findUploadsByFingerprint(_fingerprint) { + return Promise.resolve([]); + } + }, { + key: "removeUpload", + value: function removeUpload(_urlStorageKey) { + return Promise.resolve(); + } + }, { + key: "addUpload", + value: function addUpload(_fingerprint, _upload) { + return Promise.resolve(null); + } + }]); +}(); + +},{}],15:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; +var _jsBase = require("js-base64"); +var _urlParse = _interopRequireDefault(require("url-parse")); +var _error = _interopRequireDefault(require("./error.js")); +var _logger = require("./logger.js"); +var _uuid = _interopRequireDefault(require("./uuid.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return e; }; var t, e = {}, r = Object.prototype, n = r.hasOwnProperty, o = Object.defineProperty || function (t, e, r) { t[e] = r.value; }, i = "function" == typeof Symbol ? Symbol : {}, a = i.iterator || "@@iterator", c = i.asyncIterator || "@@asyncIterator", u = i.toStringTag || "@@toStringTag"; function define(t, e, r) { return Object.defineProperty(t, e, { value: r, enumerable: !0, configurable: !0, writable: !0 }), t[e]; } try { define({}, ""); } catch (t) { define = function define(t, e, r) { return t[e] = r; }; } function wrap(t, e, r, n) { var i = e && e.prototype instanceof Generator ? e : Generator, a = Object.create(i.prototype), c = new Context(n || []); return o(a, "_invoke", { value: makeInvokeMethod(t, r, c) }), a; } function tryCatch(t, e, r) { try { return { type: "normal", arg: t.call(e, r) }; } catch (t) { return { type: "throw", arg: t }; } } e.wrap = wrap; var h = "suspendedStart", l = "suspendedYield", f = "executing", s = "completed", y = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var p = {}; define(p, a, function () { return this; }); var d = Object.getPrototypeOf, v = d && d(d(values([]))); v && v !== r && n.call(v, a) && (p = v); var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p); function defineIteratorMethods(t) { ["next", "throw", "return"].forEach(function (e) { define(t, e, function (t) { return this._invoke(e, t); }); }); } function AsyncIterator(t, e) { function invoke(r, o, i, a) { var c = tryCatch(t[r], t, o); if ("throw" !== c.type) { var u = c.arg, h = u.value; return h && "object" == _typeof(h) && n.call(h, "__await") ? e.resolve(h.__await).then(function (t) { invoke("next", t, i, a); }, function (t) { invoke("throw", t, i, a); }) : e.resolve(h).then(function (t) { u.value = t, i(u); }, function (t) { return invoke("throw", t, i, a); }); } a(c.arg); } var r; o(this, "_invoke", { value: function value(t, n) { function callInvokeWithMethodAndArg() { return new e(function (e, r) { invoke(t, n, e, r); }); } return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); } }); } function makeInvokeMethod(e, r, n) { var o = h; return function (i, a) { if (o === f) throw Error("Generator is already running"); if (o === s) { if ("throw" === i) throw a; return { value: t, done: !0 }; } for (n.method = i, n.arg = a;;) { var c = n.delegate; if (c) { var u = maybeInvokeDelegate(c, n); if (u) { if (u === y) continue; return u; } } if ("next" === n.method) n.sent = n._sent = n.arg;else if ("throw" === n.method) { if (o === h) throw o = s, n.arg; n.dispatchException(n.arg); } else "return" === n.method && n.abrupt("return", n.arg); o = f; var p = tryCatch(e, r, n); if ("normal" === p.type) { if (o = n.done ? s : l, p.arg === y) continue; return { value: p.arg, done: n.done }; } "throw" === p.type && (o = s, n.method = "throw", n.arg = p.arg); } }; } function maybeInvokeDelegate(e, r) { var n = r.method, o = e.iterator[n]; if (o === t) return r.delegate = null, "throw" === n && e.iterator["return"] && (r.method = "return", r.arg = t, maybeInvokeDelegate(e, r), "throw" === r.method) || "return" !== n && (r.method = "throw", r.arg = new TypeError("The iterator does not provide a '" + n + "' method")), y; var i = tryCatch(o, e.iterator, r.arg); if ("throw" === i.type) return r.method = "throw", r.arg = i.arg, r.delegate = null, y; var a = i.arg; return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, "return" !== r.method && (r.method = "next", r.arg = t), r.delegate = null, y) : a : (r.method = "throw", r.arg = new TypeError("iterator result is not an object"), r.delegate = null, y); } function pushTryEntry(t) { var e = { tryLoc: t[0] }; 1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e); } function resetTryEntry(t) { var e = t.completion || {}; e.type = "normal", delete e.arg, t.completion = e; } function Context(t) { this.tryEntries = [{ tryLoc: "root" }], t.forEach(pushTryEntry, this), this.reset(!0); } function values(e) { if (e || "" === e) { var r = e[a]; if (r) return r.call(e); if ("function" == typeof e.next) return e; if (!isNaN(e.length)) { var o = -1, i = function next() { for (; ++o < e.length;) if (n.call(e, o)) return next.value = e[o], next.done = !1, next; return next.value = t, next.done = !0, next; }; return i.next = i; } } throw new TypeError(_typeof(e) + " is not iterable"); } return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, "constructor", { value: GeneratorFunctionPrototype, configurable: !0 }), o(GeneratorFunctionPrototype, "constructor", { value: GeneratorFunction, configurable: !0 }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, "GeneratorFunction"), e.isGeneratorFunction = function (t) { var e = "function" == typeof t && t.constructor; return !!e && (e === GeneratorFunction || "GeneratorFunction" === (e.displayName || e.name)); }, e.mark = function (t) { return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, "GeneratorFunction")), t.prototype = Object.create(g), t; }, e.awrap = function (t) { return { __await: t }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function () { return this; }), e.AsyncIterator = AsyncIterator, e.async = function (t, r, n, o, i) { void 0 === i && (i = Promise); var a = new AsyncIterator(wrap(t, r, n, o), i); return e.isGeneratorFunction(r) ? a : a.next().then(function (t) { return t.done ? t.value : a.next(); }); }, defineIteratorMethods(g), define(g, u, "Generator"), define(g, a, function () { return this; }), define(g, "toString", function () { return "[object Generator]"; }), e.keys = function (t) { var e = Object(t), r = []; for (var n in e) r.push(n); return r.reverse(), function next() { for (; r.length;) { var t = r.pop(); if (t in e) return next.value = t, next.done = !1, next; } return next.done = !0, next; }; }, e.values = values, Context.prototype = { constructor: Context, reset: function reset(e) { if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = "next", this.arg = t, this.tryEntries.forEach(resetTryEntry), !e) for (var r in this) "t" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t); }, stop: function stop() { this.done = !0; var t = this.tryEntries[0].completion; if ("throw" === t.type) throw t.arg; return this.rval; }, dispatchException: function dispatchException(e) { if (this.done) throw e; var r = this; function handle(n, o) { return a.type = "throw", a.arg = e, r.next = n, o && (r.method = "next", r.arg = t), !!o; } for (var o = this.tryEntries.length - 1; o >= 0; --o) { var i = this.tryEntries[o], a = i.completion; if ("root" === i.tryLoc) return handle("end"); if (i.tryLoc <= this.prev) { var c = n.call(i, "catchLoc"), u = n.call(i, "finallyLoc"); if (c && u) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } else if (c) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); } else { if (!u) throw Error("try statement without catch or finally"); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } } } }, abrupt: function abrupt(t, e) { for (var r = this.tryEntries.length - 1; r >= 0; --r) { var o = this.tryEntries[r]; if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) { var i = o; break; } } i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null); var a = i ? i.completion : {}; return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a); }, complete: function complete(t, e) { if ("throw" === t.type) throw t.arg; return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y; }, finish: function finish(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y; } }, "catch": function _catch(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.tryLoc === t) { var n = r.completion; if ("throw" === n.type) { var o = n.arg; resetTryEntry(r); } return o; } } throw Error("illegal catch attempt"); }, delegateYield: function delegateYield(e, r, n) { return this.delegate = { iterator: values(e), resultName: r, nextLoc: n }, "next" === this.method && (this.arg = t), y; } }, e; } +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } +function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } +function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +var PROTOCOL_TUS_V1 = 'tus-v1'; +var PROTOCOL_IETF_DRAFT_03 = 'ietf-draft-03'; +var PROTOCOL_IETF_DRAFT_05 = 'ietf-draft-05'; +var defaultOptions = { + endpoint: null, + uploadUrl: null, + metadata: {}, + metadataForPartialUploads: {}, + fingerprint: null, + uploadSize: null, + onProgress: null, + onChunkComplete: null, + onSuccess: null, + onError: null, + onUploadUrlAvailable: null, + overridePatchMethod: false, + headers: {}, + addRequestId: false, + onBeforeRequest: null, + onAfterResponse: null, + onShouldRetry: defaultOnShouldRetry, + chunkSize: Number.POSITIVE_INFINITY, + retryDelays: [0, 1000, 3000, 5000], + parallelUploads: 1, + parallelUploadBoundaries: null, + storeFingerprintForResuming: true, + removeFingerprintOnSuccess: false, + uploadLengthDeferred: false, + uploadDataDuringCreation: false, + urlStorage: null, + fileReader: null, + httpStack: null, + protocol: PROTOCOL_TUS_V1 +}; +var BaseUpload = /*#__PURE__*/function () { + function BaseUpload(file, options) { + _classCallCheck(this, BaseUpload); + // Warn about removed options from previous versions + if ('resume' in options) { + console.log('tus: The `resume` option has been removed in tus-js-client v2. Please use the URL storage API instead.'); + } + + // The default options will already be added from the wrapper classes. + this.options = options; + + // Cast chunkSize to integer + this.options.chunkSize = Number(this.options.chunkSize); + + // The storage module used to store URLs + this._urlStorage = this.options.urlStorage; + + // The underlying File/Blob object + this.file = file; + + // The URL against which the file will be uploaded + this.url = null; + + // The underlying request object for the current PATCH request + this._req = null; + + // The fingerpinrt for the current file (set after start()) + this._fingerprint = null; + + // The key that the URL storage returned when saving an URL with a fingerprint, + this._urlStorageKey = null; + + // The offset used in the current PATCH request + this._offset = null; + + // True if the current PATCH request has been aborted + this._aborted = false; + + // The file's size in bytes + this._size = null; + + // The Source object which will wrap around the given file and provides us + // with a unified interface for getting its size and slice chunks from its + // content allowing us to easily handle Files, Blobs, Buffers and Streams. + this._source = null; + + // The current count of attempts which have been made. Zero indicates none. + this._retryAttempt = 0; + + // The timeout's ID which is used to delay the next retry + this._retryTimeout = null; + + // The offset of the remote upload before the latest attempt was started. + this._offsetBeforeRetry = 0; + + // An array of BaseUpload instances which are used for uploading the different + // parts, if the parallelUploads option is used. + this._parallelUploads = null; + + // An array of upload URLs which are used for uploading the different + // parts, if the parallelUploads option is used. + this._parallelUploadUrls = null; + } + + /** + * Use the Termination extension to delete an upload from the server by sending a DELETE + * request to the specified upload URL. This is only possible if the server supports the + * Termination extension. If the `options.retryDelays` property is set, the method will + * also retry if an error ocurrs. + * + * @param {String} url The upload's URL which will be terminated. + * @param {object} options Optional options for influencing HTTP requests. + * @return {Promise} The Promise will be resolved/rejected when the requests finish. + */ + return _createClass(BaseUpload, [{ + key: "findPreviousUploads", + value: function findPreviousUploads() { + var _this = this; + return this.options.fingerprint(this.file, this.options).then(function (fingerprint) { + return _this._urlStorage.findUploadsByFingerprint(fingerprint); + }); + } + }, { + key: "resumeFromPreviousUpload", + value: function resumeFromPreviousUpload(previousUpload) { + this.url = previousUpload.uploadUrl || null; + this._parallelUploadUrls = previousUpload.parallelUploadUrls || null; + this._urlStorageKey = previousUpload.urlStorageKey; + } + }, { + key: "start", + value: function start() { + var _this2 = this; + var file = this.file; + if (!file) { + this._emitError(new Error('tus: no file or stream to upload provided')); + return; + } + if (![PROTOCOL_TUS_V1, PROTOCOL_IETF_DRAFT_03, PROTOCOL_IETF_DRAFT_05].includes(this.options.protocol)) { + this._emitError(new Error("tus: unsupported protocol ".concat(this.options.protocol))); + return; + } + if (!this.options.endpoint && !this.options.uploadUrl && !this.url) { + this._emitError(new Error('tus: neither an endpoint or an upload URL is provided')); + return; + } + var retryDelays = this.options.retryDelays; + if (retryDelays != null && Object.prototype.toString.call(retryDelays) !== '[object Array]') { + this._emitError(new Error('tus: the `retryDelays` option must either be an array or null')); + return; + } + if (this.options.parallelUploads > 1) { + // Test which options are incompatible with parallel uploads. + for (var _i = 0, _arr = ['uploadUrl', 'uploadSize', 'uploadLengthDeferred']; _i < _arr.length; _i++) { + var optionName = _arr[_i]; + if (this.options[optionName]) { + this._emitError(new Error("tus: cannot use the ".concat(optionName, " option when parallelUploads is enabled"))); + return; + } + } + } + if (this.options.parallelUploadBoundaries) { + if (this.options.parallelUploads <= 1) { + this._emitError(new Error('tus: cannot use the `parallelUploadBoundaries` option when `parallelUploads` is disabled')); + return; + } + if (this.options.parallelUploads !== this.options.parallelUploadBoundaries.length) { + this._emitError(new Error('tus: the `parallelUploadBoundaries` must have the same length as the value of `parallelUploads`')); + return; + } + } + this.options.fingerprint(file, this.options).then(function (fingerprint) { + if (fingerprint == null) { + (0, _logger.log)('No fingerprint was calculated meaning that the upload cannot be stored in the URL storage.'); + } else { + (0, _logger.log)("Calculated fingerprint: ".concat(fingerprint)); + } + _this2._fingerprint = fingerprint; + if (_this2._source) { + return _this2._source; + } + return _this2.options.fileReader.openFile(file, _this2.options.chunkSize); + }).then(function (source) { + _this2._source = source; + + // First, we look at the uploadLengthDeferred option. + // Next, we check if the caller has supplied a manual upload size. + // Finally, we try to use the calculated size from the source object. + if (_this2.options.uploadLengthDeferred) { + _this2._size = null; + } else if (_this2.options.uploadSize != null) { + _this2._size = Number(_this2.options.uploadSize); + if (Number.isNaN(_this2._size)) { + _this2._emitError(new Error('tus: cannot convert `uploadSize` option into a number')); + return; + } + } else { + _this2._size = _this2._source.size; + if (_this2._size == null) { + _this2._emitError(new Error("tus: cannot automatically derive upload's size from input. Specify it manually using the `uploadSize` option or use the `uploadLengthDeferred` option")); + return; + } + } + + // If the upload was configured to use multiple requests or if we resume from + // an upload which used multiple requests, we start a parallel upload. + if (_this2.options.parallelUploads > 1 || _this2._parallelUploadUrls != null) { + _this2._startParallelUpload(); + } else { + _this2._startSingleUpload(); + } + })["catch"](function (err) { + _this2._emitError(err); + }); + } + + /** + * Initiate the uploading procedure for a parallelized upload, where one file is split into + * multiple request which are run in parallel. + * + * @api private + */ + }, { + key: "_startParallelUpload", + value: function _startParallelUpload() { + var _this$options$paralle, + _this3 = this; + var totalSize = this._size; + var totalProgress = 0; + this._parallelUploads = []; + var partCount = this._parallelUploadUrls != null ? this._parallelUploadUrls.length : this.options.parallelUploads; + + // The input file will be split into multiple slices which are uploaded in separate + // requests. Here we get the start and end position for the slices. + var parts = (_this$options$paralle = this.options.parallelUploadBoundaries) !== null && _this$options$paralle !== void 0 ? _this$options$paralle : splitSizeIntoParts(this._source.size, partCount); + + // Attach URLs from previous uploads, if available. + if (this._parallelUploadUrls) { + parts.forEach(function (part, index) { + part.uploadUrl = _this3._parallelUploadUrls[index] || null; + }); + } + + // Create an empty list for storing the upload URLs + this._parallelUploadUrls = new Array(parts.length); + + // Generate a promise for each slice that will be resolve if the respective + // upload is completed. + var uploads = parts.map(function (part, index) { + var lastPartProgress = 0; + return _this3._source.slice(part.start, part.end).then(function (_ref) { + var value = _ref.value; + return new Promise(function (resolve, reject) { + // Merge with the user supplied options but overwrite some values. + var options = _objectSpread(_objectSpread({}, _this3.options), {}, { + // If available, the partial upload should be resumed from a previous URL. + uploadUrl: part.uploadUrl || null, + // We take manually care of resuming for partial uploads, so they should + // not be stored in the URL storage. + storeFingerprintForResuming: false, + removeFingerprintOnSuccess: false, + // Reset the parallelUploads option to not cause recursion. + parallelUploads: 1, + // Reset this option as we are not doing a parallel upload. + parallelUploadBoundaries: null, + metadata: _this3.options.metadataForPartialUploads, + // Add the header to indicate the this is a partial upload. + headers: _objectSpread(_objectSpread({}, _this3.options.headers), {}, { + 'Upload-Concat': 'partial' + }), + // Reject or resolve the promise if the upload errors or completes. + onSuccess: resolve, + onError: reject, + // Based in the progress for this partial upload, calculate the progress + // for the entire final upload. + onProgress: function onProgress(newPartProgress) { + totalProgress = totalProgress - lastPartProgress + newPartProgress; + lastPartProgress = newPartProgress; + _this3._emitProgress(totalProgress, totalSize); + }, + // Wait until every partial upload has an upload URL, so we can add + // them to the URL storage. + onUploadUrlAvailable: function onUploadUrlAvailable() { + _this3._parallelUploadUrls[index] = upload.url; + // Test if all uploads have received an URL + if (_this3._parallelUploadUrls.filter(function (u) { + return Boolean(u); + }).length === parts.length) { + _this3._saveUploadInUrlStorage(); + } + } + }); + var upload = new BaseUpload(value, options); + upload.start(); + + // Store the upload in an array, so we can later abort them if necessary. + _this3._parallelUploads.push(upload); + }); + }); + }); + var req; + // Wait until all partial uploads are finished and we can send the POST request for + // creating the final upload. + Promise.all(uploads).then(function () { + req = _this3._openRequest('POST', _this3.options.endpoint); + req.setHeader('Upload-Concat', "final;".concat(_this3._parallelUploadUrls.join(' '))); + + // Add metadata if values have been added + var metadata = encodeMetadata(_this3.options.metadata); + if (metadata !== '') { + req.setHeader('Upload-Metadata', metadata); + } + return _this3._sendRequest(req, null); + }).then(function (res) { + if (!inStatusCategory(res.getStatus(), 200)) { + _this3._emitHttpError(req, res, 'tus: unexpected response while creating upload'); + return; + } + var location = res.getHeader('Location'); + if (location == null) { + _this3._emitHttpError(req, res, 'tus: invalid or missing Location header'); + return; + } + _this3.url = resolveUrl(_this3.options.endpoint, location); + (0, _logger.log)("Created upload at ".concat(_this3.url)); + _this3._emitSuccess(res); + })["catch"](function (err) { + _this3._emitError(err); + }); + } + + /** + * Initiate the uploading procedure for a non-parallel upload. Here the entire file is + * uploaded in a sequential matter. + * + * @api private + */ + }, { + key: "_startSingleUpload", + value: function _startSingleUpload() { + // Reset the aborted flag when the upload is started or else the + // _performUpload will stop before sending a request if the upload has been + // aborted previously. + this._aborted = false; + + // The upload had been started previously and we should reuse this URL. + if (this.url != null) { + (0, _logger.log)("Resuming upload from previous URL: ".concat(this.url)); + this._resumeUpload(); + return; + } + + // A URL has manually been specified, so we try to resume + if (this.options.uploadUrl != null) { + (0, _logger.log)("Resuming upload from provided URL: ".concat(this.options.uploadUrl)); + this.url = this.options.uploadUrl; + this._resumeUpload(); + return; + } + + // An upload has not started for the file yet, so we start a new one + (0, _logger.log)('Creating a new upload'); + this._createUpload(); + } + + /** + * Abort any running request and stop the current upload. After abort is called, no event + * handler will be invoked anymore. You can use the `start` method to resume the upload + * again. + * If `shouldTerminate` is true, the `terminate` function will be called to remove the + * current upload from the server. + * + * @param {boolean} shouldTerminate True if the upload should be deleted from the server. + * @return {Promise} The Promise will be resolved/rejected when the requests finish. + */ + }, { + key: "abort", + value: function abort(shouldTerminate) { + var _this4 = this; + // Stop any parallel partial uploads, that have been started in _startParallelUploads. + if (this._parallelUploads != null) { + var _iterator = _createForOfIteratorHelper(this._parallelUploads), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var upload = _step.value; + upload.abort(shouldTerminate); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + + // Stop any current running request. + if (this._req !== null) { + this._req.abort(); + // Note: We do not close the file source here, so the user can resume in the future. + } + this._aborted = true; + + // Stop any timeout used for initiating a retry. + if (this._retryTimeout != null) { + clearTimeout(this._retryTimeout); + this._retryTimeout = null; + } + if (!shouldTerminate || this.url == null) { + return Promise.resolve(); + } + return BaseUpload.terminate(this.url, this.options) + // Remove entry from the URL storage since the upload URL is no longer valid. + .then(function () { + return _this4._removeFromUrlStorage(); + }); + } + }, { + key: "_emitHttpError", + value: function _emitHttpError(req, res, message, causingErr) { + this._emitError(new _error["default"](message, causingErr, req, res)); + } + }, { + key: "_emitError", + value: function _emitError(err) { + var _this5 = this; + // Do not emit errors, e.g. from aborted HTTP requests, if the upload has been stopped. + if (this._aborted) return; + + // Check if we should retry, when enabled, before sending the error to the user. + if (this.options.retryDelays != null) { + // We will reset the attempt counter if + // - we were already able to connect to the server (offset != null) and + // - we were able to upload a small chunk of data to the server + var shouldResetDelays = this._offset != null && this._offset > this._offsetBeforeRetry; + if (shouldResetDelays) { + this._retryAttempt = 0; + } + if (shouldRetry(err, this._retryAttempt, this.options)) { + var delay = this.options.retryDelays[this._retryAttempt++]; + this._offsetBeforeRetry = this._offset; + this._retryTimeout = setTimeout(function () { + _this5.start(); + }, delay); + return; + } + } + if (typeof this.options.onError === 'function') { + this.options.onError(err); + } else { + throw err; + } + } + + /** + * Publishes notification if the upload has been successfully completed. + * + * @param {object} lastResponse Last HTTP response. + * @api private + */ + }, { + key: "_emitSuccess", + value: function _emitSuccess(lastResponse) { + if (this.options.removeFingerprintOnSuccess) { + // Remove stored fingerprint and corresponding endpoint. This causes + // new uploads of the same file to be treated as a different file. + this._removeFromUrlStorage(); + } + if (typeof this.options.onSuccess === 'function') { + this.options.onSuccess({ + lastResponse: lastResponse + }); + } + } + + /** + * Publishes notification when data has been sent to the server. This + * data may not have been accepted by the server yet. + * + * @param {number} bytesSent Number of bytes sent to the server. + * @param {number} bytesTotal Total number of bytes to be sent to the server. + * @api private + */ + }, { + key: "_emitProgress", + value: function _emitProgress(bytesSent, bytesTotal) { + if (typeof this.options.onProgress === 'function') { + this.options.onProgress(bytesSent, bytesTotal); + } + } + + /** + * Publishes notification when a chunk of data has been sent to the server + * and accepted by the server. + * @param {number} chunkSize Size of the chunk that was accepted by the server. + * @param {number} bytesAccepted Total number of bytes that have been + * accepted by the server. + * @param {number} bytesTotal Total number of bytes to be sent to the server. + * @api private + */ + }, { + key: "_emitChunkComplete", + value: function _emitChunkComplete(chunkSize, bytesAccepted, bytesTotal) { + if (typeof this.options.onChunkComplete === 'function') { + this.options.onChunkComplete(chunkSize, bytesAccepted, bytesTotal); + } + } + + /** + * Create a new upload using the creation extension by sending a POST + * request to the endpoint. After successful creation the file will be + * uploaded + * + * @api private + */ + }, { + key: "_createUpload", + value: function _createUpload() { + var _this6 = this; + if (!this.options.endpoint) { + this._emitError(new Error('tus: unable to create upload because no endpoint is provided')); + return; + } + var req = this._openRequest('POST', this.options.endpoint); + if (this.options.uploadLengthDeferred) { + req.setHeader('Upload-Defer-Length', '1'); + } else { + req.setHeader('Upload-Length', "".concat(this._size)); + } + + // Add metadata if values have been added + var metadata = encodeMetadata(this.options.metadata); + if (metadata !== '') { + req.setHeader('Upload-Metadata', metadata); + } + var promise; + if (this.options.uploadDataDuringCreation && !this.options.uploadLengthDeferred) { + this._offset = 0; + promise = this._addChunkToRequest(req); + } else { + if (this.options.protocol === PROTOCOL_IETF_DRAFT_03 || this.options.protocol === PROTOCOL_IETF_DRAFT_05) { + req.setHeader('Upload-Complete', '?0'); + } + promise = this._sendRequest(req, null); + } + promise.then(function (res) { + if (!inStatusCategory(res.getStatus(), 200)) { + _this6._emitHttpError(req, res, 'tus: unexpected response while creating upload'); + return; + } + var location = res.getHeader('Location'); + if (location == null) { + _this6._emitHttpError(req, res, 'tus: invalid or missing Location header'); + return; + } + _this6.url = resolveUrl(_this6.options.endpoint, location); + (0, _logger.log)("Created upload at ".concat(_this6.url)); + if (typeof _this6.options.onUploadUrlAvailable === 'function') { + _this6.options.onUploadUrlAvailable(); + } + if (_this6._size === 0) { + // Nothing to upload and file was successfully created + _this6._emitSuccess(res); + _this6._source.close(); + return; + } + _this6._saveUploadInUrlStorage().then(function () { + if (_this6.options.uploadDataDuringCreation) { + _this6._handleUploadResponse(req, res); + } else { + _this6._offset = 0; + _this6._performUpload(); + } + }); + })["catch"](function (err) { + _this6._emitHttpError(req, null, 'tus: failed to create upload', err); + }); + } + + /* + * Try to resume an existing upload. First a HEAD request will be sent + * to retrieve the offset. If the request fails a new upload will be + * created. In the case of a successful response the file will be uploaded. + * + * @api private + */ + }, { + key: "_resumeUpload", + value: function _resumeUpload() { + var _this7 = this; + var req = this._openRequest('HEAD', this.url); + var promise = this._sendRequest(req, null); + promise.then(function (res) { + var status = res.getStatus(); + if (!inStatusCategory(status, 200)) { + // If the upload is locked (indicated by the 423 Locked status code), we + // emit an error instead of directly starting a new upload. This way the + // retry logic can catch the error and will retry the upload. An upload + // is usually locked for a short period of time and will be available + // afterwards. + if (status === 423) { + _this7._emitHttpError(req, res, 'tus: upload is currently locked; retry later'); + return; + } + if (inStatusCategory(status, 400)) { + // Remove stored fingerprint and corresponding endpoint, + // on client errors since the file can not be found + _this7._removeFromUrlStorage(); + } + if (!_this7.options.endpoint) { + // Don't attempt to create a new upload if no endpoint is provided. + _this7._emitHttpError(req, res, 'tus: unable to resume upload (new upload cannot be created without an endpoint)'); + return; + } + + // Try to create a new upload + _this7.url = null; + _this7._createUpload(); + return; + } + var offset = Number.parseInt(res.getHeader('Upload-Offset'), 10); + if (Number.isNaN(offset)) { + _this7._emitHttpError(req, res, 'tus: invalid or missing offset value'); + return; + } + var length = Number.parseInt(res.getHeader('Upload-Length'), 10); + if (Number.isNaN(length) && !_this7.options.uploadLengthDeferred && _this7.options.protocol === PROTOCOL_TUS_V1) { + _this7._emitHttpError(req, res, 'tus: invalid or missing length value'); + return; + } + if (typeof _this7.options.onUploadUrlAvailable === 'function') { + _this7.options.onUploadUrlAvailable(); + } + _this7._saveUploadInUrlStorage().then(function () { + // Upload has already been completed and we do not need to send additional + // data to the server + if (offset === length) { + _this7._emitProgress(length, length); + _this7._emitSuccess(res); + return; + } + _this7._offset = offset; + _this7._performUpload(); + }); + })["catch"](function (err) { + _this7._emitHttpError(req, null, 'tus: failed to resume upload', err); + }); + } + + /** + * Start uploading the file using PATCH requests. The file will be divided + * into chunks as specified in the chunkSize option. During the upload + * the onProgress event handler may be invoked multiple times. + * + * @api private + */ + }, { + key: "_performUpload", + value: function _performUpload() { + var _this8 = this; + // If the upload has been aborted, we will not send the next PATCH request. + // This is important if the abort method was called during a callback, such + // as onChunkComplete or onProgress. + if (this._aborted) { + return; + } + var req; + + // Some browser and servers may not support the PATCH method. For those + // cases, you can tell tus-js-client to use a POST request with the + // X-HTTP-Method-Override header for simulating a PATCH request. + if (this.options.overridePatchMethod) { + req = this._openRequest('POST', this.url); + req.setHeader('X-HTTP-Method-Override', 'PATCH'); + } else { + req = this._openRequest('PATCH', this.url); + } + req.setHeader('Upload-Offset', "".concat(this._offset)); + var promise = this._addChunkToRequest(req); + promise.then(function (res) { + if (!inStatusCategory(res.getStatus(), 200)) { + _this8._emitHttpError(req, res, 'tus: unexpected response while uploading chunk'); + return; + } + _this8._handleUploadResponse(req, res); + })["catch"](function (err) { + // Don't emit an error if the upload was aborted manually + if (_this8._aborted) { + return; + } + _this8._emitHttpError(req, null, "tus: failed to upload chunk at offset ".concat(_this8._offset), err); + }); + } + + /** + * _addChunktoRequest reads a chunk from the source and sends it using the + * supplied request object. It will not handle the response. + * + * @api private + */ + }, { + key: "_addChunkToRequest", + value: function _addChunkToRequest(req) { + var _this9 = this; + var start = this._offset; + var end = this._offset + this.options.chunkSize; + req.setProgressHandler(function (bytesSent) { + _this9._emitProgress(start + bytesSent, _this9._size); + }); + if (this.options.protocol === PROTOCOL_TUS_V1) { + req.setHeader('Content-Type', 'application/offset+octet-stream'); + } else if (this.options.protocol === PROTOCOL_IETF_DRAFT_05) { + req.setHeader('Content-Type', 'application/partial-upload'); + } + + // The specified chunkSize may be Infinity or the calcluated end position + // may exceed the file's size. In both cases, we limit the end position to + // the input's total size for simpler calculations and correctness. + if ((end === Number.POSITIVE_INFINITY || end > this._size) && !this.options.uploadLengthDeferred) { + end = this._size; + } + return this._source.slice(start, end).then(function (_ref2) { + var value = _ref2.value, + done = _ref2.done; + var valueSize = value !== null && value !== void 0 && value.size ? value.size : 0; + + // If the upload length is deferred, the upload size was not specified during + // upload creation. So, if the file reader is done reading, we know the total + // upload size and can tell the tus server. + if (_this9.options.uploadLengthDeferred && done) { + _this9._size = _this9._offset + valueSize; + req.setHeader('Upload-Length', "".concat(_this9._size)); + } + + // The specified uploadSize might not match the actual amount of data that a source + // provides. In these cases, we cannot successfully complete the upload, so we + // rather error out and let the user know. If not, tus-js-client will be stuck + // in a loop of repeating empty PATCH requests. + // See https://community.transloadit.com/t/how-to-abort-hanging-companion-uploads/16488/13 + var newSize = _this9._offset + valueSize; + if (!_this9.options.uploadLengthDeferred && done && newSize !== _this9._size) { + return Promise.reject(new Error("upload was configured with a size of ".concat(_this9._size, " bytes, but the source is done after ").concat(newSize, " bytes"))); + } + if (value === null) { + return _this9._sendRequest(req); + } + if (_this9.options.protocol === PROTOCOL_IETF_DRAFT_03 || _this9.options.protocol === PROTOCOL_IETF_DRAFT_05) { + req.setHeader('Upload-Complete', done ? '?1' : '?0'); + } + _this9._emitProgress(_this9._offset, _this9._size); + return _this9._sendRequest(req, value); + }); + } + + /** + * _handleUploadResponse is used by requests that haven been sent using _addChunkToRequest + * and already have received a response. + * + * @api private + */ + }, { + key: "_handleUploadResponse", + value: function _handleUploadResponse(req, res) { + var offset = Number.parseInt(res.getHeader('Upload-Offset'), 10); + if (Number.isNaN(offset)) { + this._emitHttpError(req, res, 'tus: invalid or missing offset value'); + return; + } + this._emitProgress(offset, this._size); + this._emitChunkComplete(offset - this._offset, offset, this._size); + this._offset = offset; + if (offset === this._size) { + // Yay, finally done :) + this._emitSuccess(res); + this._source.close(); + return; + } + this._performUpload(); + } + + /** + * Create a new HTTP request object with the given method and URL. + * + * @api private + */ + }, { + key: "_openRequest", + value: function _openRequest(method, url) { + var req = openRequest(method, url, this.options); + this._req = req; + return req; + } + + /** + * Remove the entry in the URL storage, if it has been saved before. + * + * @api private + */ + }, { + key: "_removeFromUrlStorage", + value: function _removeFromUrlStorage() { + var _this10 = this; + if (!this._urlStorageKey) return; + this._urlStorage.removeUpload(this._urlStorageKey)["catch"](function (err) { + _this10._emitError(err); + }); + this._urlStorageKey = null; + } + + /** + * Add the upload URL to the URL storage, if possible. + * + * @api private + */ + }, { + key: "_saveUploadInUrlStorage", + value: function _saveUploadInUrlStorage() { + var _this11 = this; + // We do not store the upload URL + // - if it was disabled in the option, or + // - if no fingerprint was calculated for the input (i.e. a stream), or + // - if the URL is already stored (i.e. key is set alread). + if (!this.options.storeFingerprintForResuming || !this._fingerprint || this._urlStorageKey !== null) { + return Promise.resolve(); + } + var storedUpload = { + size: this._size, + metadata: this.options.metadata, + creationTime: new Date().toString() + }; + if (this._parallelUploads) { + // Save multiple URLs if the parallelUploads option is used ... + storedUpload.parallelUploadUrls = this._parallelUploadUrls; + } else { + // ... otherwise we just save the one available URL. + storedUpload.uploadUrl = this.url; + } + return this._urlStorage.addUpload(this._fingerprint, storedUpload).then(function (urlStorageKey) { + _this11._urlStorageKey = urlStorageKey; + }); + } + + /** + * Send a request with the provided body. + * + * @api private + */ + }, { + key: "_sendRequest", + value: function _sendRequest(req) { + var body = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + return sendRequest(req, body, this.options); + } + }], [{ + key: "terminate", + value: function terminate(url) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var req = openRequest('DELETE', url, options); + return sendRequest(req, null, options).then(function (res) { + // A 204 response indicates a successfull request + if (res.getStatus() === 204) { + return; + } + throw new _error["default"]('tus: unexpected response while terminating upload', null, req, res); + })["catch"](function (err) { + if (!(err instanceof _error["default"])) { + err = new _error["default"]('tus: failed to terminate upload', err, req, null); + } + if (!shouldRetry(err, 0, options)) { + throw err; + } + + // Instead of keeping track of the retry attempts, we remove the first element from the delays + // array. If the array is empty, all retry attempts are used up and we will bubble up the error. + // We recursively call the terminate function will removing elements from the retryDelays array. + var delay = options.retryDelays[0]; + var remainingDelays = options.retryDelays.slice(1); + var newOptions = _objectSpread(_objectSpread({}, options), {}, { + retryDelays: remainingDelays + }); + return new Promise(function (resolve) { + return setTimeout(resolve, delay); + }).then(function () { + return BaseUpload.terminate(url, newOptions); + }); + }); + } + }]); +}(); +function encodeMetadata(metadata) { + return Object.entries(metadata).map(function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + key = _ref4[0], + value = _ref4[1]; + return "".concat(key, " ").concat(_jsBase.Base64.encode(String(value))); + }).join(','); +} + +/** + * Checks whether a given status is in the range of the expected category. + * For example, only a status between 200 and 299 will satisfy the category 200. + * + * @api private + */ +function inStatusCategory(status, category) { + return status >= category && status < category + 100; +} + +/** + * Create a new HTTP request with the specified method and URL. + * The necessary headers that are included in every request + * will be added, including the request ID. + * + * @api private + */ +function openRequest(method, url, options) { + var req = options.httpStack.createRequest(method, url); + if (options.protocol === PROTOCOL_IETF_DRAFT_03) { + req.setHeader('Upload-Draft-Interop-Version', '5'); + } else if (options.protocol === PROTOCOL_IETF_DRAFT_05) { + req.setHeader('Upload-Draft-Interop-Version', '6'); + } else { + req.setHeader('Tus-Resumable', '1.0.0'); + } + var headers = options.headers || {}; + for (var _i2 = 0, _Object$entries = Object.entries(headers); _i2 < _Object$entries.length; _i2++) { + var _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2), + name = _Object$entries$_i[0], + value = _Object$entries$_i[1]; + req.setHeader(name, value); + } + if (options.addRequestId) { + var requestId = (0, _uuid["default"])(); + req.setHeader('X-Request-ID', requestId); + } + return req; +} + +/** + * Send a request with the provided body while invoking the onBeforeRequest + * and onAfterResponse callbacks. + * + * @api private + */ +function sendRequest(_x, _x2, _x3) { + return _sendRequest2.apply(this, arguments); +} +/** + * Checks whether the browser running this code has internet access. + * This function will always return true in the node.js environment + * + * @api private + */ +function _sendRequest2() { + _sendRequest2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(req, body, options) { + var res; + return _regeneratorRuntime().wrap(function _callee$(_context) { + while (1) switch (_context.prev = _context.next) { + case 0: + if (!(typeof options.onBeforeRequest === 'function')) { + _context.next = 3; + break; + } + _context.next = 3; + return options.onBeforeRequest(req); + case 3: + _context.next = 5; + return req.send(body); + case 5: + res = _context.sent; + if (!(typeof options.onAfterResponse === 'function')) { + _context.next = 9; + break; + } + _context.next = 9; + return options.onAfterResponse(req, res); + case 9: + return _context.abrupt("return", res); + case 10: + case "end": + return _context.stop(); + } + }, _callee); + })); + return _sendRequest2.apply(this, arguments); +} +function isOnline() { + var online = true; + // Note: We don't reference `window` here because the navigator object also exists + // in a Web Worker's context. + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + online = false; + } + return online; +} + +/** + * Checks whether or not it is ok to retry a request. + * @param {Error|DetailedError} err the error returned from the last request + * @param {number} retryAttempt the number of times the request has already been retried + * @param {object} options tus Upload options + * + * @api private + */ +function shouldRetry(err, retryAttempt, options) { + // We only attempt a retry if + // - retryDelays option is set + // - we didn't exceed the maxium number of retries, yet, and + // - this error was caused by a request or it's response and + // - the error is server error (i.e. not a status 4xx except a 409 or 423) or + // a onShouldRetry is specified and returns true + // - the browser does not indicate that we are offline + if (options.retryDelays == null || retryAttempt >= options.retryDelays.length || err.originalRequest == null) { + return false; + } + if (options && typeof options.onShouldRetry === 'function') { + return options.onShouldRetry(err, retryAttempt, options); + } + return defaultOnShouldRetry(err); +} + +/** + * determines if the request should be retried. Will only retry if not a status 4xx except a 409 or 423 + * @param {DetailedError} err + * @returns {boolean} + */ +function defaultOnShouldRetry(err) { + var status = err.originalResponse ? err.originalResponse.getStatus() : 0; + return (!inStatusCategory(status, 400) || status === 409 || status === 423) && isOnline(); +} + +/** + * Resolve a relative link given the origin as source. For example, + * if a HTTP request to http://example.com/files/ returns a Location + * header with the value /upload/abc, the resolved URL will be: + * http://example.com/upload/abc + */ +function resolveUrl(origin, link) { + return new _urlParse["default"](link, origin).toString(); +} + +/** + * Calculate the start and end positions for the parts if an upload + * is split into multiple parallel requests. + * + * @param {number} totalSize The byte size of the upload, which will be split. + * @param {number} partCount The number in how many parts the upload will be split. + * @return {object[]} + * @api private + */ +function splitSizeIntoParts(totalSize, partCount) { + var partSize = Math.floor(totalSize / partCount); + var parts = []; + for (var i = 0; i < partCount; i++) { + parts.push({ + start: partSize * i, + end: partSize * (i + 1) + }); + } + parts[partCount - 1].end = totalSize; + return parts; +} +BaseUpload.defaultOptions = defaultOptions; +var _default = exports["default"] = BaseUpload; + +},{"./error.js":12,"./logger.js":13,"./uuid.js":16,"js-base64":20,"url-parse":23}],16:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = uuid; +/** + * Generate a UUID v4 based on random numbers. We intentioanlly use the less + * secure Math.random function here since the more secure crypto.getRandomNumbers + * is not available on all platforms. + * This is not a problem for us since we use the UUID only for generating a + * request ID, so we can correlate server logs to client errors. + * + * This function is taken from following site: + * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript + * + * @return {string} The generate UUID + */ +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0; + var v = c === 'x' ? r : r & 0x3 | 0x8; + return v.toString(16); + }); +} + +},{}],17:[function(require,module,exports){ +'use strict' + +exports.byteLength = byteLength +exports.toByteArray = toByteArray +exports.fromByteArray = fromByteArray + +var lookup = [] +var revLookup = [] +var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array + +var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +for (var i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i] + revLookup[code.charCodeAt(i)] = i +} + +// Support decoding URL-safe base64 strings, as Node.js does. +// See: https://en.wikipedia.org/wiki/Base64#URL_applications +revLookup['-'.charCodeAt(0)] = 62 +revLookup['_'.charCodeAt(0)] = 63 + +function getLens (b64) { + var len = b64.length + + if (len % 4 > 0) { + throw new Error('Invalid string. Length must be a multiple of 4') + } + + // Trim off extra bytes after placeholder bytes are found + // See: https://github.com/beatgammit/base64-js/issues/42 + var validLen = b64.indexOf('=') + if (validLen === -1) validLen = len + + var placeHoldersLen = validLen === len + ? 0 + : 4 - (validLen % 4) + + return [validLen, placeHoldersLen] +} + +// base64 is 4/3 + up to two characters of the original data +function byteLength (b64) { + var lens = getLens(b64) + var validLen = lens[0] + var placeHoldersLen = lens[1] + return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen +} + +function _byteLength (b64, validLen, placeHoldersLen) { + return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen +} + +function toByteArray (b64) { + var tmp + var lens = getLens(b64) + var validLen = lens[0] + var placeHoldersLen = lens[1] + + var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) + + var curByte = 0 + + // if there are placeholders, only get up to the last complete 4 chars + var len = placeHoldersLen > 0 + ? validLen - 4 + : validLen + + var i + for (i = 0; i < len; i += 4) { + tmp = + (revLookup[b64.charCodeAt(i)] << 18) | + (revLookup[b64.charCodeAt(i + 1)] << 12) | + (revLookup[b64.charCodeAt(i + 2)] << 6) | + revLookup[b64.charCodeAt(i + 3)] + arr[curByte++] = (tmp >> 16) & 0xFF + arr[curByte++] = (tmp >> 8) & 0xFF + arr[curByte++] = tmp & 0xFF + } + + if (placeHoldersLen === 2) { + tmp = + (revLookup[b64.charCodeAt(i)] << 2) | + (revLookup[b64.charCodeAt(i + 1)] >> 4) + arr[curByte++] = tmp & 0xFF + } + + if (placeHoldersLen === 1) { + tmp = + (revLookup[b64.charCodeAt(i)] << 10) | + (revLookup[b64.charCodeAt(i + 1)] << 4) | + (revLookup[b64.charCodeAt(i + 2)] >> 2) + arr[curByte++] = (tmp >> 8) & 0xFF + arr[curByte++] = tmp & 0xFF + } + + return arr +} + +function tripletToBase64 (num) { + return lookup[num >> 18 & 0x3F] + + lookup[num >> 12 & 0x3F] + + lookup[num >> 6 & 0x3F] + + lookup[num & 0x3F] +} + +function encodeChunk (uint8, start, end) { + var tmp + var output = [] + for (var i = start; i < end; i += 3) { + tmp = + ((uint8[i] << 16) & 0xFF0000) + + ((uint8[i + 1] << 8) & 0xFF00) + + (uint8[i + 2] & 0xFF) + output.push(tripletToBase64(tmp)) + } + return output.join('') +} + +function fromByteArray (uint8) { + var tmp + var len = uint8.length + var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes + var parts = [] + var maxChunkLength = 16383 // must be multiple of 3 + + // go through the array every three bytes, we'll deal with trailing stuff later + for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { + parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) + } + + // pad the end with zeros, but make sure to not forget the extra bytes + if (extraBytes === 1) { + tmp = uint8[len - 1] + parts.push( + lookup[tmp >> 2] + + lookup[(tmp << 4) & 0x3F] + + '==' + ) + } else if (extraBytes === 2) { + tmp = (uint8[len - 2] << 8) + uint8[len - 1] + parts.push( + lookup[tmp >> 10] + + lookup[(tmp >> 4) & 0x3F] + + lookup[(tmp << 2) & 0x3F] + + '=' + ) + } + + return parts.join('') +} + +},{}],18:[function(require,module,exports){ +(function (Buffer){(function (){ +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ +/* eslint-disable no-proto */ + +'use strict' + +var base64 = require('base64-js') +var ieee754 = require('ieee754') + +exports.Buffer = Buffer +exports.SlowBuffer = SlowBuffer +exports.INSPECT_MAX_BYTES = 50 + +var K_MAX_LENGTH = 0x7fffffff +exports.kMaxLength = K_MAX_LENGTH + +/** + * If `Buffer.TYPED_ARRAY_SUPPORT`: + * === true Use Uint8Array implementation (fastest) + * === false Print warning and recommend using `buffer` v4.x which has an Object + * implementation (most compatible, even IE6) + * + * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, + * Opera 11.6+, iOS 4.2+. + * + * We report that the browser does not support typed arrays if the are not subclassable + * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array` + * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support + * for __proto__ and has a buggy typed array implementation. + */ +Buffer.TYPED_ARRAY_SUPPORT = typedArraySupport() + +if (!Buffer.TYPED_ARRAY_SUPPORT && typeof console !== 'undefined' && + typeof console.error === 'function') { + console.error( + 'This browser lacks typed array (Uint8Array) support which is required by ' + + '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.' + ) +} + +function typedArraySupport () { + // Can typed array instances can be augmented? + try { + var arr = new Uint8Array(1) + arr.__proto__ = { __proto__: Uint8Array.prototype, foo: function () { return 42 } } + return arr.foo() === 42 + } catch (e) { + return false + } +} + +Object.defineProperty(Buffer.prototype, 'parent', { + enumerable: true, + get: function () { + if (!Buffer.isBuffer(this)) return undefined + return this.buffer + } +}) + +Object.defineProperty(Buffer.prototype, 'offset', { + enumerable: true, + get: function () { + if (!Buffer.isBuffer(this)) return undefined + return this.byteOffset + } +}) + +function createBuffer (length) { + if (length > K_MAX_LENGTH) { + throw new RangeError('The value "' + length + '" is invalid for option "size"') + } + // Return an augmented `Uint8Array` instance + var buf = new Uint8Array(length) + buf.__proto__ = Buffer.prototype + return buf +} + +/** + * The Buffer constructor returns instances of `Uint8Array` that have their + * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of + * `Uint8Array`, so the returned instances will have all the node `Buffer` methods + * and the `Uint8Array` methods. Square bracket notation works as expected -- it + * returns a single octet. + * + * The `Uint8Array` prototype remains unmodified. + */ + +function Buffer (arg, encodingOrOffset, length) { + // Common case. + if (typeof arg === 'number') { + if (typeof encodingOrOffset === 'string') { + throw new TypeError( + 'The "string" argument must be of type string. Received type number' + ) + } + return allocUnsafe(arg) + } + return from(arg, encodingOrOffset, length) +} + +// Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 +if (typeof Symbol !== 'undefined' && Symbol.species != null && + Buffer[Symbol.species] === Buffer) { + Object.defineProperty(Buffer, Symbol.species, { + value: null, + configurable: true, + enumerable: false, + writable: false + }) +} + +Buffer.poolSize = 8192 // not used by this implementation + +function from (value, encodingOrOffset, length) { + if (typeof value === 'string') { + return fromString(value, encodingOrOffset) + } + + if (ArrayBuffer.isView(value)) { + return fromArrayLike(value) + } + + if (value == null) { + throw TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + + 'or Array-like Object. Received type ' + (typeof value) + ) + } + + if (isInstance(value, ArrayBuffer) || + (value && isInstance(value.buffer, ArrayBuffer))) { + return fromArrayBuffer(value, encodingOrOffset, length) + } + + if (typeof value === 'number') { + throw new TypeError( + 'The "value" argument must not be of type number. Received type number' + ) + } + + var valueOf = value.valueOf && value.valueOf() + if (valueOf != null && valueOf !== value) { + return Buffer.from(valueOf, encodingOrOffset, length) + } + + var b = fromObject(value) + if (b) return b + + if (typeof Symbol !== 'undefined' && Symbol.toPrimitive != null && + typeof value[Symbol.toPrimitive] === 'function') { + return Buffer.from( + value[Symbol.toPrimitive]('string'), encodingOrOffset, length + ) + } + + throw new TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + + 'or Array-like Object. Received type ' + (typeof value) + ) +} + +/** + * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError + * if value is a number. + * Buffer.from(str[, encoding]) + * Buffer.from(array) + * Buffer.from(buffer) + * Buffer.from(arrayBuffer[, byteOffset[, length]]) + **/ +Buffer.from = function (value, encodingOrOffset, length) { + return from(value, encodingOrOffset, length) +} + +// Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug: +// https://github.com/feross/buffer/pull/148 +Buffer.prototype.__proto__ = Uint8Array.prototype +Buffer.__proto__ = Uint8Array + +function assertSize (size) { + if (typeof size !== 'number') { + throw new TypeError('"size" argument must be of type number') + } else if (size < 0) { + throw new RangeError('The value "' + size + '" is invalid for option "size"') + } +} + +function alloc (size, fill, encoding) { + assertSize(size) + if (size <= 0) { + return createBuffer(size) + } + if (fill !== undefined) { + // Only pay attention to encoding if it's a string. This + // prevents accidentally sending in a number that would + // be interpretted as a start offset. + return typeof encoding === 'string' + ? createBuffer(size).fill(fill, encoding) + : createBuffer(size).fill(fill) + } + return createBuffer(size) +} + +/** + * Creates a new filled Buffer instance. + * alloc(size[, fill[, encoding]]) + **/ +Buffer.alloc = function (size, fill, encoding) { + return alloc(size, fill, encoding) +} + +function allocUnsafe (size) { + assertSize(size) + return createBuffer(size < 0 ? 0 : checked(size) | 0) +} + +/** + * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. + * */ +Buffer.allocUnsafe = function (size) { + return allocUnsafe(size) +} +/** + * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. + */ +Buffer.allocUnsafeSlow = function (size) { + return allocUnsafe(size) +} + +function fromString (string, encoding) { + if (typeof encoding !== 'string' || encoding === '') { + encoding = 'utf8' + } + + if (!Buffer.isEncoding(encoding)) { + throw new TypeError('Unknown encoding: ' + encoding) + } + + var length = byteLength(string, encoding) | 0 + var buf = createBuffer(length) + + var actual = buf.write(string, encoding) + + if (actual !== length) { + // Writing a hex string, for example, that contains invalid characters will + // cause everything after the first invalid character to be ignored. (e.g. + // 'abxxcd' will be treated as 'ab') + buf = buf.slice(0, actual) + } + + return buf +} + +function fromArrayLike (array) { + var length = array.length < 0 ? 0 : checked(array.length) | 0 + var buf = createBuffer(length) + for (var i = 0; i < length; i += 1) { + buf[i] = array[i] & 255 + } + return buf +} + +function fromArrayBuffer (array, byteOffset, length) { + if (byteOffset < 0 || array.byteLength < byteOffset) { + throw new RangeError('"offset" is outside of buffer bounds') + } + + if (array.byteLength < byteOffset + (length || 0)) { + throw new RangeError('"length" is outside of buffer bounds') + } + + var buf + if (byteOffset === undefined && length === undefined) { + buf = new Uint8Array(array) + } else if (length === undefined) { + buf = new Uint8Array(array, byteOffset) + } else { + buf = new Uint8Array(array, byteOffset, length) + } + + // Return an augmented `Uint8Array` instance + buf.__proto__ = Buffer.prototype + return buf +} + +function fromObject (obj) { + if (Buffer.isBuffer(obj)) { + var len = checked(obj.length) | 0 + var buf = createBuffer(len) + + if (buf.length === 0) { + return buf + } + + obj.copy(buf, 0, 0, len) + return buf + } + + if (obj.length !== undefined) { + if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) { + return createBuffer(0) + } + return fromArrayLike(obj) + } + + if (obj.type === 'Buffer' && Array.isArray(obj.data)) { + return fromArrayLike(obj.data) + } +} + +function checked (length) { + // Note: cannot use `length < K_MAX_LENGTH` here because that fails when + // length is NaN (which is otherwise coerced to zero.) + if (length >= K_MAX_LENGTH) { + throw new RangeError('Attempt to allocate Buffer larger than maximum ' + + 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes') + } + return length | 0 +} + +function SlowBuffer (length) { + if (+length != length) { // eslint-disable-line eqeqeq + length = 0 + } + return Buffer.alloc(+length) +} + +Buffer.isBuffer = function isBuffer (b) { + return b != null && b._isBuffer === true && + b !== Buffer.prototype // so Buffer.isBuffer(Buffer.prototype) will be false +} + +Buffer.compare = function compare (a, b) { + if (isInstance(a, Uint8Array)) a = Buffer.from(a, a.offset, a.byteLength) + if (isInstance(b, Uint8Array)) b = Buffer.from(b, b.offset, b.byteLength) + if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { + throw new TypeError( + 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' + ) + } + + if (a === b) return 0 + + var x = a.length + var y = b.length + + for (var i = 0, len = Math.min(x, y); i < len; ++i) { + if (a[i] !== b[i]) { + x = a[i] + y = b[i] + break + } + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 +} + +Buffer.isEncoding = function isEncoding (encoding) { + switch (String(encoding).toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'latin1': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return true + default: + return false + } +} + +Buffer.concat = function concat (list, length) { + if (!Array.isArray(list)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + + if (list.length === 0) { + return Buffer.alloc(0) + } + + var i + if (length === undefined) { + length = 0 + for (i = 0; i < list.length; ++i) { + length += list[i].length + } + } + + var buffer = Buffer.allocUnsafe(length) + var pos = 0 + for (i = 0; i < list.length; ++i) { + var buf = list[i] + if (isInstance(buf, Uint8Array)) { + buf = Buffer.from(buf) + } + if (!Buffer.isBuffer(buf)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + buf.copy(buffer, pos) + pos += buf.length + } + return buffer +} + +function byteLength (string, encoding) { + if (Buffer.isBuffer(string)) { + return string.length + } + if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { + return string.byteLength + } + if (typeof string !== 'string') { + throw new TypeError( + 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. ' + + 'Received type ' + typeof string + ) + } + + var len = string.length + var mustMatch = (arguments.length > 2 && arguments[2] === true) + if (!mustMatch && len === 0) return 0 + + // Use a for loop to avoid recursion + var loweredCase = false + for (;;) { + switch (encoding) { + case 'ascii': + case 'latin1': + case 'binary': + return len + case 'utf8': + case 'utf-8': + return utf8ToBytes(string).length + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return len * 2 + case 'hex': + return len >>> 1 + case 'base64': + return base64ToBytes(string).length + default: + if (loweredCase) { + return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8 + } + encoding = ('' + encoding).toLowerCase() + loweredCase = true + } + } +} +Buffer.byteLength = byteLength + +function slowToString (encoding, start, end) { + var loweredCase = false + + // No need to verify that "this.length <= MAX_UINT32" since it's a read-only + // property of a typed array. + + // This behaves neither like String nor Uint8Array in that we set start/end + // to their upper/lower bounds if the value passed is out of range. + // undefined is handled specially as per ECMA-262 6th Edition, + // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. + if (start === undefined || start < 0) { + start = 0 + } + // Return early if start > this.length. Done here to prevent potential uint32 + // coercion fail below. + if (start > this.length) { + return '' + } + + if (end === undefined || end > this.length) { + end = this.length + } + + if (end <= 0) { + return '' + } + + // Force coersion to uint32. This will also coerce falsey/NaN values to 0. + end >>>= 0 + start >>>= 0 + + if (end <= start) { + return '' + } + + if (!encoding) encoding = 'utf8' + + while (true) { + switch (encoding) { + case 'hex': + return hexSlice(this, start, end) + + case 'utf8': + case 'utf-8': + return utf8Slice(this, start, end) + + case 'ascii': + return asciiSlice(this, start, end) + + case 'latin1': + case 'binary': + return latin1Slice(this, start, end) + + case 'base64': + return base64Slice(this, start, end) + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return utf16leSlice(this, start, end) + + default: + if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) + encoding = (encoding + '').toLowerCase() + loweredCase = true + } + } +} + +// This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package) +// to detect a Buffer instance. It's not possible to use `instanceof Buffer` +// reliably in a browserify context because there could be multiple different +// copies of the 'buffer' package in use. This method works even for Buffer +// instances that were created from another copy of the `buffer` package. +// See: https://github.com/feross/buffer/issues/154 +Buffer.prototype._isBuffer = true + +function swap (b, n, m) { + var i = b[n] + b[n] = b[m] + b[m] = i +} + +Buffer.prototype.swap16 = function swap16 () { + var len = this.length + if (len % 2 !== 0) { + throw new RangeError('Buffer size must be a multiple of 16-bits') + } + for (var i = 0; i < len; i += 2) { + swap(this, i, i + 1) + } + return this +} + +Buffer.prototype.swap32 = function swap32 () { + var len = this.length + if (len % 4 !== 0) { + throw new RangeError('Buffer size must be a multiple of 32-bits') + } + for (var i = 0; i < len; i += 4) { + swap(this, i, i + 3) + swap(this, i + 1, i + 2) + } + return this +} + +Buffer.prototype.swap64 = function swap64 () { + var len = this.length + if (len % 8 !== 0) { + throw new RangeError('Buffer size must be a multiple of 64-bits') + } + for (var i = 0; i < len; i += 8) { + swap(this, i, i + 7) + swap(this, i + 1, i + 6) + swap(this, i + 2, i + 5) + swap(this, i + 3, i + 4) + } + return this +} + +Buffer.prototype.toString = function toString () { + var length = this.length + if (length === 0) return '' + if (arguments.length === 0) return utf8Slice(this, 0, length) + return slowToString.apply(this, arguments) +} + +Buffer.prototype.toLocaleString = Buffer.prototype.toString + +Buffer.prototype.equals = function equals (b) { + if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer') + if (this === b) return true + return Buffer.compare(this, b) === 0 +} + +Buffer.prototype.inspect = function inspect () { + var str = '' + var max = exports.INSPECT_MAX_BYTES + str = this.toString('hex', 0, max).replace(/(.{2})/g, '$1 ').trim() + if (this.length > max) str += ' ... ' + return '' +} + +Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { + if (isInstance(target, Uint8Array)) { + target = Buffer.from(target, target.offset, target.byteLength) + } + if (!Buffer.isBuffer(target)) { + throw new TypeError( + 'The "target" argument must be one of type Buffer or Uint8Array. ' + + 'Received type ' + (typeof target) + ) + } + + if (start === undefined) { + start = 0 + } + if (end === undefined) { + end = target ? target.length : 0 + } + if (thisStart === undefined) { + thisStart = 0 + } + if (thisEnd === undefined) { + thisEnd = this.length + } + + if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { + throw new RangeError('out of range index') + } + + if (thisStart >= thisEnd && start >= end) { + return 0 + } + if (thisStart >= thisEnd) { + return -1 + } + if (start >= end) { + return 1 + } + + start >>>= 0 + end >>>= 0 + thisStart >>>= 0 + thisEnd >>>= 0 + + if (this === target) return 0 + + var x = thisEnd - thisStart + var y = end - start + var len = Math.min(x, y) + + var thisCopy = this.slice(thisStart, thisEnd) + var targetCopy = target.slice(start, end) + + for (var i = 0; i < len; ++i) { + if (thisCopy[i] !== targetCopy[i]) { + x = thisCopy[i] + y = targetCopy[i] + break + } + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 +} + +// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, +// OR the last index of `val` in `buffer` at offset <= `byteOffset`. +// +// Arguments: +// - buffer - a Buffer to search +// - val - a string, Buffer, or number +// - byteOffset - an index into `buffer`; will be clamped to an int32 +// - encoding - an optional encoding, relevant is val is a string +// - dir - true for indexOf, false for lastIndexOf +function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { + // Empty buffer means no match + if (buffer.length === 0) return -1 + + // Normalize byteOffset + if (typeof byteOffset === 'string') { + encoding = byteOffset + byteOffset = 0 + } else if (byteOffset > 0x7fffffff) { + byteOffset = 0x7fffffff + } else if (byteOffset < -0x80000000) { + byteOffset = -0x80000000 + } + byteOffset = +byteOffset // Coerce to Number. + if (numberIsNaN(byteOffset)) { + // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer + byteOffset = dir ? 0 : (buffer.length - 1) + } + + // Normalize byteOffset: negative offsets start from the end of the buffer + if (byteOffset < 0) byteOffset = buffer.length + byteOffset + if (byteOffset >= buffer.length) { + if (dir) return -1 + else byteOffset = buffer.length - 1 + } else if (byteOffset < 0) { + if (dir) byteOffset = 0 + else return -1 + } + + // Normalize val + if (typeof val === 'string') { + val = Buffer.from(val, encoding) + } + + // Finally, search either indexOf (if dir is true) or lastIndexOf + if (Buffer.isBuffer(val)) { + // Special case: looking for empty string/buffer always fails + if (val.length === 0) { + return -1 + } + return arrayIndexOf(buffer, val, byteOffset, encoding, dir) + } else if (typeof val === 'number') { + val = val & 0xFF // Search for a byte value [0-255] + if (typeof Uint8Array.prototype.indexOf === 'function') { + if (dir) { + return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) + } else { + return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) + } + } + return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir) + } + + throw new TypeError('val must be string, number or Buffer') +} + +function arrayIndexOf (arr, val, byteOffset, encoding, dir) { + var indexSize = 1 + var arrLength = arr.length + var valLength = val.length + + if (encoding !== undefined) { + encoding = String(encoding).toLowerCase() + if (encoding === 'ucs2' || encoding === 'ucs-2' || + encoding === 'utf16le' || encoding === 'utf-16le') { + if (arr.length < 2 || val.length < 2) { + return -1 + } + indexSize = 2 + arrLength /= 2 + valLength /= 2 + byteOffset /= 2 + } + } + + function read (buf, i) { + if (indexSize === 1) { + return buf[i] + } else { + return buf.readUInt16BE(i * indexSize) + } + } + + var i + if (dir) { + var foundIndex = -1 + for (i = byteOffset; i < arrLength; i++) { + if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { + if (foundIndex === -1) foundIndex = i + if (i - foundIndex + 1 === valLength) return foundIndex * indexSize + } else { + if (foundIndex !== -1) i -= i - foundIndex + foundIndex = -1 + } + } + } else { + if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength + for (i = byteOffset; i >= 0; i--) { + var found = true + for (var j = 0; j < valLength; j++) { + if (read(arr, i + j) !== read(val, j)) { + found = false + break + } + } + if (found) return i + } + } + + return -1 +} + +Buffer.prototype.includes = function includes (val, byteOffset, encoding) { + return this.indexOf(val, byteOffset, encoding) !== -1 +} + +Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, true) +} + +Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, false) +} + +function hexWrite (buf, string, offset, length) { + offset = Number(offset) || 0 + var remaining = buf.length - offset + if (!length) { + length = remaining + } else { + length = Number(length) + if (length > remaining) { + length = remaining + } + } + + var strLen = string.length + + if (length > strLen / 2) { + length = strLen / 2 + } + for (var i = 0; i < length; ++i) { + var parsed = parseInt(string.substr(i * 2, 2), 16) + if (numberIsNaN(parsed)) return i + buf[offset + i] = parsed + } + return i +} + +function utf8Write (buf, string, offset, length) { + return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) +} + +function asciiWrite (buf, string, offset, length) { + return blitBuffer(asciiToBytes(string), buf, offset, length) +} + +function latin1Write (buf, string, offset, length) { + return asciiWrite(buf, string, offset, length) +} + +function base64Write (buf, string, offset, length) { + return blitBuffer(base64ToBytes(string), buf, offset, length) +} + +function ucs2Write (buf, string, offset, length) { + return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) +} + +Buffer.prototype.write = function write (string, offset, length, encoding) { + // Buffer#write(string) + if (offset === undefined) { + encoding = 'utf8' + length = this.length + offset = 0 + // Buffer#write(string, encoding) + } else if (length === undefined && typeof offset === 'string') { + encoding = offset + length = this.length + offset = 0 + // Buffer#write(string, offset[, length][, encoding]) + } else if (isFinite(offset)) { + offset = offset >>> 0 + if (isFinite(length)) { + length = length >>> 0 + if (encoding === undefined) encoding = 'utf8' + } else { + encoding = length + length = undefined + } + } else { + throw new Error( + 'Buffer.write(string, encoding, offset[, length]) is no longer supported' + ) + } + + var remaining = this.length - offset + if (length === undefined || length > remaining) length = remaining + + if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { + throw new RangeError('Attempt to write outside buffer bounds') + } + + if (!encoding) encoding = 'utf8' + + var loweredCase = false + for (;;) { + switch (encoding) { + case 'hex': + return hexWrite(this, string, offset, length) + + case 'utf8': + case 'utf-8': + return utf8Write(this, string, offset, length) + + case 'ascii': + return asciiWrite(this, string, offset, length) + + case 'latin1': + case 'binary': + return latin1Write(this, string, offset, length) + + case 'base64': + // Warning: maxLength not taken into account in base64Write + return base64Write(this, string, offset, length) + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return ucs2Write(this, string, offset, length) + + default: + if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) + encoding = ('' + encoding).toLowerCase() + loweredCase = true + } + } +} + +Buffer.prototype.toJSON = function toJSON () { + return { + type: 'Buffer', + data: Array.prototype.slice.call(this._arr || this, 0) + } +} + +function base64Slice (buf, start, end) { + if (start === 0 && end === buf.length) { + return base64.fromByteArray(buf) + } else { + return base64.fromByteArray(buf.slice(start, end)) + } +} + +function utf8Slice (buf, start, end) { + end = Math.min(buf.length, end) + var res = [] + + var i = start + while (i < end) { + var firstByte = buf[i] + var codePoint = null + var bytesPerSequence = (firstByte > 0xEF) ? 4 + : (firstByte > 0xDF) ? 3 + : (firstByte > 0xBF) ? 2 + : 1 + + if (i + bytesPerSequence <= end) { + var secondByte, thirdByte, fourthByte, tempCodePoint + + switch (bytesPerSequence) { + case 1: + if (firstByte < 0x80) { + codePoint = firstByte + } + break + case 2: + secondByte = buf[i + 1] + if ((secondByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F) + if (tempCodePoint > 0x7F) { + codePoint = tempCodePoint + } + } + break + case 3: + secondByte = buf[i + 1] + thirdByte = buf[i + 2] + if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F) + if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { + codePoint = tempCodePoint + } + } + break + case 4: + secondByte = buf[i + 1] + thirdByte = buf[i + 2] + fourthByte = buf[i + 3] + if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F) + if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { + codePoint = tempCodePoint + } + } + } + } + + if (codePoint === null) { + // we did not generate a valid codePoint so insert a + // replacement char (U+FFFD) and advance only 1 byte + codePoint = 0xFFFD + bytesPerSequence = 1 + } else if (codePoint > 0xFFFF) { + // encode to utf16 (surrogate pair dance) + codePoint -= 0x10000 + res.push(codePoint >>> 10 & 0x3FF | 0xD800) + codePoint = 0xDC00 | codePoint & 0x3FF + } + + res.push(codePoint) + i += bytesPerSequence + } + + return decodeCodePointsArray(res) +} + +// Based on http://stackoverflow.com/a/22747272/680742, the browser with +// the lowest limit is Chrome, with 0x10000 args. +// We go 1 magnitude less, for safety +var MAX_ARGUMENTS_LENGTH = 0x1000 + +function decodeCodePointsArray (codePoints) { + var len = codePoints.length + if (len <= MAX_ARGUMENTS_LENGTH) { + return String.fromCharCode.apply(String, codePoints) // avoid extra slice() + } + + // Decode in chunks to avoid "call stack size exceeded". + var res = '' + var i = 0 + while (i < len) { + res += String.fromCharCode.apply( + String, + codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) + ) + } + return res +} + +function asciiSlice (buf, start, end) { + var ret = '' + end = Math.min(buf.length, end) + + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i] & 0x7F) + } + return ret +} + +function latin1Slice (buf, start, end) { + var ret = '' + end = Math.min(buf.length, end) + + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i]) + } + return ret +} + +function hexSlice (buf, start, end) { + var len = buf.length + + if (!start || start < 0) start = 0 + if (!end || end < 0 || end > len) end = len + + var out = '' + for (var i = start; i < end; ++i) { + out += toHex(buf[i]) + } + return out +} + +function utf16leSlice (buf, start, end) { + var bytes = buf.slice(start, end) + var res = '' + for (var i = 0; i < bytes.length; i += 2) { + res += String.fromCharCode(bytes[i] + (bytes[i + 1] * 256)) + } + return res +} + +Buffer.prototype.slice = function slice (start, end) { + var len = this.length + start = ~~start + end = end === undefined ? len : ~~end + + if (start < 0) { + start += len + if (start < 0) start = 0 + } else if (start > len) { + start = len + } + + if (end < 0) { + end += len + if (end < 0) end = 0 + } else if (end > len) { + end = len + } + + if (end < start) end = start + + var newBuf = this.subarray(start, end) + // Return an augmented `Uint8Array` instance + newBuf.__proto__ = Buffer.prototype + return newBuf +} + +/* + * Need to make sure that buffer isn't trying to write out of bounds. + */ +function checkOffset (offset, ext, length) { + if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') + if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') +} + +Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) checkOffset(offset, byteLength, this.length) + + var val = this[offset] + var mul = 1 + var i = 0 + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul + } + + return val +} + +Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) { + checkOffset(offset, byteLength, this.length) + } + + var val = this[offset + --byteLength] + var mul = 1 + while (byteLength > 0 && (mul *= 0x100)) { + val += this[offset + --byteLength] * mul + } + + return val +} + +Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 1, this.length) + return this[offset] +} + +Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + return this[offset] | (this[offset + 1] << 8) +} + +Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + return (this[offset] << 8) | this[offset + 1] +} + +Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return ((this[offset]) | + (this[offset + 1] << 8) | + (this[offset + 2] << 16)) + + (this[offset + 3] * 0x1000000) +} + +Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return (this[offset] * 0x1000000) + + ((this[offset + 1] << 16) | + (this[offset + 2] << 8) | + this[offset + 3]) +} + +Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) checkOffset(offset, byteLength, this.length) + + var val = this[offset] + var mul = 1 + var i = 0 + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul + } + mul *= 0x80 + + if (val >= mul) val -= Math.pow(2, 8 * byteLength) + + return val +} + +Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) checkOffset(offset, byteLength, this.length) + + var i = byteLength + var mul = 1 + var val = this[offset + --i] + while (i > 0 && (mul *= 0x100)) { + val += this[offset + --i] * mul + } + mul *= 0x80 + + if (val >= mul) val -= Math.pow(2, 8 * byteLength) + + return val +} + +Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 1, this.length) + if (!(this[offset] & 0x80)) return (this[offset]) + return ((0xff - this[offset] + 1) * -1) +} + +Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + var val = this[offset] | (this[offset + 1] << 8) + return (val & 0x8000) ? val | 0xFFFF0000 : val +} + +Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + var val = this[offset + 1] | (this[offset] << 8) + return (val & 0x8000) ? val | 0xFFFF0000 : val +} + +Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return (this[offset]) | + (this[offset + 1] << 8) | + (this[offset + 2] << 16) | + (this[offset + 3] << 24) +} + +Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return (this[offset] << 24) | + (this[offset + 1] << 16) | + (this[offset + 2] << 8) | + (this[offset + 3]) +} + +Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + return ieee754.read(this, offset, true, 23, 4) +} + +Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + return ieee754.read(this, offset, false, 23, 4) +} + +Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 8, this.length) + return ieee754.read(this, offset, true, 52, 8) +} + +Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 8, this.length) + return ieee754.read(this, offset, false, 52, 8) +} + +function checkInt (buf, value, offset, ext, max, min) { + if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') + if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') + if (offset + ext > buf.length) throw new RangeError('Index out of range') +} + +Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength) - 1 + checkInt(this, value, offset, byteLength, maxBytes, 0) + } + + var mul = 1 + var i = 0 + this[offset] = value & 0xFF + while (++i < byteLength && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength) - 1 + checkInt(this, value, offset, byteLength, maxBytes, 0) + } + + var i = byteLength - 1 + var mul = 1 + this[offset + i] = value & 0xFF + while (--i >= 0 && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0) + this[offset] = (value & 0xff) + return offset + 1 +} + +Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) + this[offset] = (value & 0xff) + this[offset + 1] = (value >>> 8) + return offset + 2 +} + +Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) + this[offset] = (value >>> 8) + this[offset + 1] = (value & 0xff) + return offset + 2 +} + +Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) + this[offset + 3] = (value >>> 24) + this[offset + 2] = (value >>> 16) + this[offset + 1] = (value >>> 8) + this[offset] = (value & 0xff) + return offset + 4 +} + +Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) + this[offset] = (value >>> 24) + this[offset + 1] = (value >>> 16) + this[offset + 2] = (value >>> 8) + this[offset + 3] = (value & 0xff) + return offset + 4 +} + +Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + var limit = Math.pow(2, (8 * byteLength) - 1) + + checkInt(this, value, offset, byteLength, limit - 1, -limit) + } + + var i = 0 + var mul = 1 + var sub = 0 + this[offset] = value & 0xFF + while (++i < byteLength && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { + sub = 1 + } + this[offset + i] = ((value / mul) >> 0) - sub & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + var limit = Math.pow(2, (8 * byteLength) - 1) + + checkInt(this, value, offset, byteLength, limit - 1, -limit) + } + + var i = byteLength - 1 + var mul = 1 + var sub = 0 + this[offset + i] = value & 0xFF + while (--i >= 0 && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { + sub = 1 + } + this[offset + i] = ((value / mul) >> 0) - sub & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80) + if (value < 0) value = 0xff + value + 1 + this[offset] = (value & 0xff) + return offset + 1 +} + +Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) + this[offset] = (value & 0xff) + this[offset + 1] = (value >>> 8) + return offset + 2 +} + +Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) + this[offset] = (value >>> 8) + this[offset + 1] = (value & 0xff) + return offset + 2 +} + +Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) + this[offset] = (value & 0xff) + this[offset + 1] = (value >>> 8) + this[offset + 2] = (value >>> 16) + this[offset + 3] = (value >>> 24) + return offset + 4 +} + +Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) + if (value < 0) value = 0xffffffff + value + 1 + this[offset] = (value >>> 24) + this[offset + 1] = (value >>> 16) + this[offset + 2] = (value >>> 8) + this[offset + 3] = (value & 0xff) + return offset + 4 +} + +function checkIEEE754 (buf, value, offset, ext, max, min) { + if (offset + ext > buf.length) throw new RangeError('Index out of range') + if (offset < 0) throw new RangeError('Index out of range') +} + +function writeFloat (buf, value, offset, littleEndian, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38) + } + ieee754.write(buf, value, offset, littleEndian, 23, 4) + return offset + 4 +} + +Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { + return writeFloat(this, value, offset, true, noAssert) +} + +Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { + return writeFloat(this, value, offset, false, noAssert) +} + +function writeDouble (buf, value, offset, littleEndian, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308) + } + ieee754.write(buf, value, offset, littleEndian, 52, 8) + return offset + 8 +} + +Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { + return writeDouble(this, value, offset, true, noAssert) +} + +Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { + return writeDouble(this, value, offset, false, noAssert) +} + +// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) +Buffer.prototype.copy = function copy (target, targetStart, start, end) { + if (!Buffer.isBuffer(target)) throw new TypeError('argument should be a Buffer') + if (!start) start = 0 + if (!end && end !== 0) end = this.length + if (targetStart >= target.length) targetStart = target.length + if (!targetStart) targetStart = 0 + if (end > 0 && end < start) end = start + + // Copy 0 bytes; we're done + if (end === start) return 0 + if (target.length === 0 || this.length === 0) return 0 + + // Fatal error conditions + if (targetStart < 0) { + throw new RangeError('targetStart out of bounds') + } + if (start < 0 || start >= this.length) throw new RangeError('Index out of range') + if (end < 0) throw new RangeError('sourceEnd out of bounds') + + // Are we oob? + if (end > this.length) end = this.length + if (target.length - targetStart < end - start) { + end = target.length - targetStart + start + } + + var len = end - start + + if (this === target && typeof Uint8Array.prototype.copyWithin === 'function') { + // Use built-in when available, missing from IE11 + this.copyWithin(targetStart, start, end) + } else if (this === target && start < targetStart && targetStart < end) { + // descending copy from end + for (var i = len - 1; i >= 0; --i) { + target[i + targetStart] = this[i + start] + } + } else { + Uint8Array.prototype.set.call( + target, + this.subarray(start, end), + targetStart + ) + } + + return len +} + +// Usage: +// buffer.fill(number[, offset[, end]]) +// buffer.fill(buffer[, offset[, end]]) +// buffer.fill(string[, offset[, end]][, encoding]) +Buffer.prototype.fill = function fill (val, start, end, encoding) { + // Handle string cases: + if (typeof val === 'string') { + if (typeof start === 'string') { + encoding = start + start = 0 + end = this.length + } else if (typeof end === 'string') { + encoding = end + end = this.length + } + if (encoding !== undefined && typeof encoding !== 'string') { + throw new TypeError('encoding must be a string') + } + if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { + throw new TypeError('Unknown encoding: ' + encoding) + } + if (val.length === 1) { + var code = val.charCodeAt(0) + if ((encoding === 'utf8' && code < 128) || + encoding === 'latin1') { + // Fast path: If `val` fits into a single byte, use that numeric value. + val = code + } + } + } else if (typeof val === 'number') { + val = val & 255 + } + + // Invalid ranges are not set to a default, so can range check early. + if (start < 0 || this.length < start || this.length < end) { + throw new RangeError('Out of range index') + } + + if (end <= start) { + return this + } + + start = start >>> 0 + end = end === undefined ? this.length : end >>> 0 + + if (!val) val = 0 + + var i + if (typeof val === 'number') { + for (i = start; i < end; ++i) { + this[i] = val + } + } else { + var bytes = Buffer.isBuffer(val) + ? val + : Buffer.from(val, encoding) + var len = bytes.length + if (len === 0) { + throw new TypeError('The value "' + val + + '" is invalid for argument "value"') + } + for (i = 0; i < end - start; ++i) { + this[i + start] = bytes[i % len] + } + } + + return this +} + +// HELPER FUNCTIONS +// ================ + +var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g + +function base64clean (str) { + // Node takes equal signs as end of the Base64 encoding + str = str.split('=')[0] + // Node strips out invalid characters like \n and \t from the string, base64-js does not + str = str.trim().replace(INVALID_BASE64_RE, '') + // Node converts strings with length < 2 to '' + if (str.length < 2) return '' + // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not + while (str.length % 4 !== 0) { + str = str + '=' + } + return str +} + +function toHex (n) { + if (n < 16) return '0' + n.toString(16) + return n.toString(16) +} + +function utf8ToBytes (string, units) { + units = units || Infinity + var codePoint + var length = string.length + var leadSurrogate = null + var bytes = [] + + for (var i = 0; i < length; ++i) { + codePoint = string.charCodeAt(i) + + // is surrogate component + if (codePoint > 0xD7FF && codePoint < 0xE000) { + // last char was a lead + if (!leadSurrogate) { + // no lead yet + if (codePoint > 0xDBFF) { + // unexpected trail + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + continue + } else if (i + 1 === length) { + // unpaired lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + continue + } + + // valid lead + leadSurrogate = codePoint + + continue + } + + // 2 leads in a row + if (codePoint < 0xDC00) { + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + leadSurrogate = codePoint + continue + } + + // valid surrogate pair + codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000 + } else if (leadSurrogate) { + // valid bmp char, but last char was a lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + } + + leadSurrogate = null + + // encode utf8 + if (codePoint < 0x80) { + if ((units -= 1) < 0) break + bytes.push(codePoint) + } else if (codePoint < 0x800) { + if ((units -= 2) < 0) break + bytes.push( + codePoint >> 0x6 | 0xC0, + codePoint & 0x3F | 0x80 + ) + } else if (codePoint < 0x10000) { + if ((units -= 3) < 0) break + bytes.push( + codePoint >> 0xC | 0xE0, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ) + } else if (codePoint < 0x110000) { + if ((units -= 4) < 0) break + bytes.push( + codePoint >> 0x12 | 0xF0, + codePoint >> 0xC & 0x3F | 0x80, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ) + } else { + throw new Error('Invalid code point') + } + } + + return bytes +} + +function asciiToBytes (str) { + var byteArray = [] + for (var i = 0; i < str.length; ++i) { + // Node's code seems to be doing this and not & 0x7F.. + byteArray.push(str.charCodeAt(i) & 0xFF) + } + return byteArray +} + +function utf16leToBytes (str, units) { + var c, hi, lo + var byteArray = [] + for (var i = 0; i < str.length; ++i) { + if ((units -= 2) < 0) break + + c = str.charCodeAt(i) + hi = c >> 8 + lo = c % 256 + byteArray.push(lo) + byteArray.push(hi) + } + + return byteArray +} + +function base64ToBytes (str) { + return base64.toByteArray(base64clean(str)) +} + +function blitBuffer (src, dst, offset, length) { + for (var i = 0; i < length; ++i) { + if ((i + offset >= dst.length) || (i >= src.length)) break + dst[i + offset] = src[i] + } + return i +} + +// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass +// the `instanceof` check but they should be treated as of that type. +// See: https://github.com/feross/buffer/issues/166 +function isInstance (obj, type) { + return obj instanceof type || + (obj != null && obj.constructor != null && obj.constructor.name != null && + obj.constructor.name === type.name) +} +function numberIsNaN (obj) { + // For IE11 support + return obj !== obj // eslint-disable-line no-self-compare +} + +}).call(this)}).call(this,require("buffer").Buffer) + +},{"base64-js":17,"buffer":18,"ieee754":19}],19:[function(require,module,exports){ +/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ +exports.read = function (buffer, offset, isLE, mLen, nBytes) { + var e, m + var eLen = (nBytes * 8) - mLen - 1 + var eMax = (1 << eLen) - 1 + var eBias = eMax >> 1 + var nBits = -7 + var i = isLE ? (nBytes - 1) : 0 + var d = isLE ? -1 : 1 + var s = buffer[offset + i] + + i += d + + e = s & ((1 << (-nBits)) - 1) + s >>= (-nBits) + nBits += eLen + for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {} + + m = e & ((1 << (-nBits)) - 1) + e >>= (-nBits) + nBits += mLen + for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {} + + if (e === 0) { + e = 1 - eBias + } else if (e === eMax) { + return m ? NaN : ((s ? -1 : 1) * Infinity) + } else { + m = m + Math.pow(2, mLen) + e = e - eBias + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen) +} + +exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { + var e, m, c + var eLen = (nBytes * 8) - mLen - 1 + var eMax = (1 << eLen) - 1 + var eBias = eMax >> 1 + var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) + var i = isLE ? 0 : (nBytes - 1) + var d = isLE ? 1 : -1 + var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 + + value = Math.abs(value) + + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0 + e = eMax + } else { + e = Math.floor(Math.log(value) / Math.LN2) + if (value * (c = Math.pow(2, -e)) < 1) { + e-- + c *= 2 + } + if (e + eBias >= 1) { + value += rt / c + } else { + value += rt * Math.pow(2, 1 - eBias) + } + if (value * c >= 2) { + e++ + c /= 2 + } + + if (e + eBias >= eMax) { + m = 0 + e = eMax + } else if (e + eBias >= 1) { + m = ((value * c) - 1) * Math.pow(2, mLen) + e = e + eBias + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) + e = 0 + } + } + + for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {} + + e = (e << mLen) | m + eLen += mLen + for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {} + + buffer[offset + i - d] |= s * 128 +} + +},{}],20:[function(require,module,exports){ +(function (global,Buffer){(function (){ +// +// THIS FILE IS AUTOMATICALLY GENERATED! DO NOT EDIT BY HAND! +// +; +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? module.exports = factory() + : typeof define === 'function' && define.amd + ? define(factory) : + // cf. https://github.com/dankogai/js-base64/issues/119 + (function () { + // existing version for noConflict() + var _Base64 = global.Base64; + var gBase64 = factory(); + gBase64.noConflict = function () { + global.Base64 = _Base64; + return gBase64; + }; + if (global.Meteor) { // Meteor.js + Base64 = gBase64; + } + global.Base64 = gBase64; + })(); +}((typeof self !== 'undefined' ? self + : typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : this), function () { + 'use strict'; + /** + * base64.ts + * + * Licensed under the BSD 3-Clause License. + * http://opensource.org/licenses/BSD-3-Clause + * + * References: + * http://en.wikipedia.org/wiki/Base64 + * + * @author Dan Kogai (https://github.com/dankogai) + */ + var version = '3.7.7'; + /** + * @deprecated use lowercase `version`. + */ + var VERSION = version; + var _hasBuffer = typeof Buffer === 'function'; + var _TD = typeof TextDecoder === 'function' ? new TextDecoder() : undefined; + var _TE = typeof TextEncoder === 'function' ? new TextEncoder() : undefined; + var b64ch = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var b64chs = Array.prototype.slice.call(b64ch); + var b64tab = (function (a) { + var tab = {}; + a.forEach(function (c, i) { return tab[c] = i; }); + return tab; + })(b64chs); + var b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/; + var _fromCC = String.fromCharCode.bind(String); + var _U8Afrom = typeof Uint8Array.from === 'function' + ? Uint8Array.from.bind(Uint8Array) + : function (it) { return new Uint8Array(Array.prototype.slice.call(it, 0)); }; + var _mkUriSafe = function (src) { return src + .replace(/=/g, '').replace(/[+\/]/g, function (m0) { return m0 == '+' ? '-' : '_'; }); }; + var _tidyB64 = function (s) { return s.replace(/[^A-Za-z0-9\+\/]/g, ''); }; + /** + * polyfill version of `btoa` + */ + var btoaPolyfill = function (bin) { + // console.log('polyfilled'); + var u32, c0, c1, c2, asc = ''; + var pad = bin.length % 3; + for (var i = 0; i < bin.length;) { + if ((c0 = bin.charCodeAt(i++)) > 255 || + (c1 = bin.charCodeAt(i++)) > 255 || + (c2 = bin.charCodeAt(i++)) > 255) + throw new TypeError('invalid character found'); + u32 = (c0 << 16) | (c1 << 8) | c2; + asc += b64chs[u32 >> 18 & 63] + + b64chs[u32 >> 12 & 63] + + b64chs[u32 >> 6 & 63] + + b64chs[u32 & 63]; + } + return pad ? asc.slice(0, pad - 3) + "===".substring(pad) : asc; + }; + /** + * does what `window.btoa` of web browsers do. + * @param {String} bin binary string + * @returns {string} Base64-encoded string + */ + var _btoa = typeof btoa === 'function' ? function (bin) { return btoa(bin); } + : _hasBuffer ? function (bin) { return Buffer.from(bin, 'binary').toString('base64'); } + : btoaPolyfill; + var _fromUint8Array = _hasBuffer + ? function (u8a) { return Buffer.from(u8a).toString('base64'); } + : function (u8a) { + // cf. https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string/12713326#12713326 + var maxargs = 0x1000; + var strs = []; + for (var i = 0, l = u8a.length; i < l; i += maxargs) { + strs.push(_fromCC.apply(null, u8a.subarray(i, i + maxargs))); + } + return _btoa(strs.join('')); + }; + /** + * converts a Uint8Array to a Base64 string. + * @param {boolean} [urlsafe] URL-and-filename-safe a la RFC4648 §5 + * @returns {string} Base64 string + */ + var fromUint8Array = function (u8a, urlsafe) { + if (urlsafe === void 0) { urlsafe = false; } + return urlsafe ? _mkUriSafe(_fromUint8Array(u8a)) : _fromUint8Array(u8a); + }; + // This trick is found broken https://github.com/dankogai/js-base64/issues/130 + // const utob = (src: string) => unescape(encodeURIComponent(src)); + // reverting good old fationed regexp + var cb_utob = function (c) { + if (c.length < 2) { + var cc = c.charCodeAt(0); + return cc < 0x80 ? c + : cc < 0x800 ? (_fromCC(0xc0 | (cc >>> 6)) + + _fromCC(0x80 | (cc & 0x3f))) + : (_fromCC(0xe0 | ((cc >>> 12) & 0x0f)) + + _fromCC(0x80 | ((cc >>> 6) & 0x3f)) + + _fromCC(0x80 | (cc & 0x3f))); + } + else { + var cc = 0x10000 + + (c.charCodeAt(0) - 0xD800) * 0x400 + + (c.charCodeAt(1) - 0xDC00); + return (_fromCC(0xf0 | ((cc >>> 18) & 0x07)) + + _fromCC(0x80 | ((cc >>> 12) & 0x3f)) + + _fromCC(0x80 | ((cc >>> 6) & 0x3f)) + + _fromCC(0x80 | (cc & 0x3f))); + } + }; + var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; + /** + * @deprecated should have been internal use only. + * @param {string} src UTF-8 string + * @returns {string} UTF-16 string + */ + var utob = function (u) { return u.replace(re_utob, cb_utob); }; + // + var _encode = _hasBuffer + ? function (s) { return Buffer.from(s, 'utf8').toString('base64'); } + : _TE + ? function (s) { return _fromUint8Array(_TE.encode(s)); } + : function (s) { return _btoa(utob(s)); }; + /** + * converts a UTF-8-encoded string to a Base64 string. + * @param {boolean} [urlsafe] if `true` make the result URL-safe + * @returns {string} Base64 string + */ + var encode = function (src, urlsafe) { + if (urlsafe === void 0) { urlsafe = false; } + return urlsafe + ? _mkUriSafe(_encode(src)) + : _encode(src); + }; + /** + * converts a UTF-8-encoded string to URL-safe Base64 RFC4648 §5. + * @returns {string} Base64 string + */ + var encodeURI = function (src) { return encode(src, true); }; + // This trick is found broken https://github.com/dankogai/js-base64/issues/130 + // const btou = (src: string) => decodeURIComponent(escape(src)); + // reverting good old fationed regexp + var re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g; + var cb_btou = function (cccc) { + switch (cccc.length) { + case 4: + var cp = ((0x07 & cccc.charCodeAt(0)) << 18) + | ((0x3f & cccc.charCodeAt(1)) << 12) + | ((0x3f & cccc.charCodeAt(2)) << 6) + | (0x3f & cccc.charCodeAt(3)), offset = cp - 0x10000; + return (_fromCC((offset >>> 10) + 0xD800) + + _fromCC((offset & 0x3FF) + 0xDC00)); + case 3: + return _fromCC(((0x0f & cccc.charCodeAt(0)) << 12) + | ((0x3f & cccc.charCodeAt(1)) << 6) + | (0x3f & cccc.charCodeAt(2))); + default: + return _fromCC(((0x1f & cccc.charCodeAt(0)) << 6) + | (0x3f & cccc.charCodeAt(1))); + } + }; + /** + * @deprecated should have been internal use only. + * @param {string} src UTF-16 string + * @returns {string} UTF-8 string + */ + var btou = function (b) { return b.replace(re_btou, cb_btou); }; + /** + * polyfill version of `atob` + */ + var atobPolyfill = function (asc) { + // console.log('polyfilled'); + asc = asc.replace(/\s+/g, ''); + if (!b64re.test(asc)) + throw new TypeError('malformed base64.'); + asc += '=='.slice(2 - (asc.length & 3)); + var u24, bin = '', r1, r2; + for (var i = 0; i < asc.length;) { + u24 = b64tab[asc.charAt(i++)] << 18 + | b64tab[asc.charAt(i++)] << 12 + | (r1 = b64tab[asc.charAt(i++)]) << 6 + | (r2 = b64tab[asc.charAt(i++)]); + bin += r1 === 64 ? _fromCC(u24 >> 16 & 255) + : r2 === 64 ? _fromCC(u24 >> 16 & 255, u24 >> 8 & 255) + : _fromCC(u24 >> 16 & 255, u24 >> 8 & 255, u24 & 255); + } + return bin; + }; + /** + * does what `window.atob` of web browsers do. + * @param {String} asc Base64-encoded string + * @returns {string} binary string + */ + var _atob = typeof atob === 'function' ? function (asc) { return atob(_tidyB64(asc)); } + : _hasBuffer ? function (asc) { return Buffer.from(asc, 'base64').toString('binary'); } + : atobPolyfill; + // + var _toUint8Array = _hasBuffer + ? function (a) { return _U8Afrom(Buffer.from(a, 'base64')); } + : function (a) { return _U8Afrom(_atob(a).split('').map(function (c) { return c.charCodeAt(0); })); }; + /** + * converts a Base64 string to a Uint8Array. + */ + var toUint8Array = function (a) { return _toUint8Array(_unURI(a)); }; + // + var _decode = _hasBuffer + ? function (a) { return Buffer.from(a, 'base64').toString('utf8'); } + : _TD + ? function (a) { return _TD.decode(_toUint8Array(a)); } + : function (a) { return btou(_atob(a)); }; + var _unURI = function (a) { return _tidyB64(a.replace(/[-_]/g, function (m0) { return m0 == '-' ? '+' : '/'; })); }; + /** + * converts a Base64 string to a UTF-8 string. + * @param {String} src Base64 string. Both normal and URL-safe are supported + * @returns {string} UTF-8 string + */ + var decode = function (src) { return _decode(_unURI(src)); }; + /** + * check if a value is a valid Base64 string + * @param {String} src a value to check + */ + var isValid = function (src) { + if (typeof src !== 'string') + return false; + var s = src.replace(/\s+/g, '').replace(/={0,2}$/, ''); + return !/[^\s0-9a-zA-Z\+/]/.test(s) || !/[^\s0-9a-zA-Z\-_]/.test(s); + }; + // + var _noEnum = function (v) { + return { + value: v, enumerable: false, writable: true, configurable: true + }; + }; + /** + * extend String.prototype with relevant methods + */ + var extendString = function () { + var _add = function (name, body) { return Object.defineProperty(String.prototype, name, _noEnum(body)); }; + _add('fromBase64', function () { return decode(this); }); + _add('toBase64', function (urlsafe) { return encode(this, urlsafe); }); + _add('toBase64URI', function () { return encode(this, true); }); + _add('toBase64URL', function () { return encode(this, true); }); + _add('toUint8Array', function () { return toUint8Array(this); }); + }; + /** + * extend Uint8Array.prototype with relevant methods + */ + var extendUint8Array = function () { + var _add = function (name, body) { return Object.defineProperty(Uint8Array.prototype, name, _noEnum(body)); }; + _add('toBase64', function (urlsafe) { return fromUint8Array(this, urlsafe); }); + _add('toBase64URI', function () { return fromUint8Array(this, true); }); + _add('toBase64URL', function () { return fromUint8Array(this, true); }); + }; + /** + * extend Builtin prototypes with relevant methods + */ + var extendBuiltins = function () { + extendString(); + extendUint8Array(); + }; + var gBase64 = { + version: version, + VERSION: VERSION, + atob: _atob, + atobPolyfill: atobPolyfill, + btoa: _btoa, + btoaPolyfill: btoaPolyfill, + fromBase64: decode, + toBase64: encode, + encode: encode, + encodeURI: encodeURI, + encodeURL: encodeURI, + utob: utob, + btou: btou, + decode: decode, + isValid: isValid, + fromUint8Array: fromUint8Array, + toUint8Array: toUint8Array, + extendString: extendString, + extendUint8Array: extendUint8Array, + extendBuiltins: extendBuiltins + }; + // + // export Base64 to the namespace + // + // ES5 is yet to have Object.assign() that may make transpilers unhappy. + // gBase64.Base64 = Object.assign({}, gBase64); + gBase64.Base64 = {}; + Object.keys(gBase64).forEach(function (k) { return gBase64.Base64[k] = gBase64[k]; }); + return gBase64; +})); + +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("buffer").Buffer) + +},{"buffer":18}],21:[function(require,module,exports){ +'use strict'; + +var has = Object.prototype.hasOwnProperty + , undef; + +/** + * Decode a URI encoded string. + * + * @param {String} input The URI encoded string. + * @returns {String|Null} The decoded string. + * @api private + */ +function decode(input) { + try { + return decodeURIComponent(input.replace(/\+/g, ' ')); + } catch (e) { + return null; + } +} + +/** + * Attempts to encode a given input. + * + * @param {String} input The string that needs to be encoded. + * @returns {String|Null} The encoded string. + * @api private + */ +function encode(input) { + try { + return encodeURIComponent(input); + } catch (e) { + return null; + } +} + +/** + * Simple query string parser. + * + * @param {String} query The query string that needs to be parsed. + * @returns {Object} + * @api public + */ +function querystring(query) { + var parser = /([^=?#&]+)=?([^&]*)/g + , result = {} + , part; + + while (part = parser.exec(query)) { + var key = decode(part[1]) + , value = decode(part[2]); + + // + // Prevent overriding of existing properties. This ensures that build-in + // methods like `toString` or __proto__ are not overriden by malicious + // querystrings. + // + // In the case if failed decoding, we want to omit the key/value pairs + // from the result. + // + if (key === null || value === null || key in result) continue; + result[key] = value; + } + + return result; +} + +/** + * Transform a query string to an object. + * + * @param {Object} obj Object that should be transformed. + * @param {String} prefix Optional prefix. + * @returns {String} + * @api public + */ +function querystringify(obj, prefix) { + prefix = prefix || ''; + + var pairs = [] + , value + , key; + + // + // Optionally prefix with a '?' if needed + // + if ('string' !== typeof prefix) prefix = '?'; + + for (key in obj) { + if (has.call(obj, key)) { + value = obj[key]; + + // + // Edge cases where we actually want to encode the value to an empty + // string instead of the stringified value. + // + if (!value && (value === null || value === undef || isNaN(value))) { + value = ''; + } + + key = encode(key); + value = encode(value); + + // + // If we failed to encode the strings, we should bail out as we don't + // want to add invalid strings to the query. + // + if (key === null || value === null) continue; + pairs.push(key +'='+ value); + } + } + + return pairs.length ? prefix + pairs.join('&') : ''; +} + +// +// Expose the module. +// +exports.stringify = querystringify; +exports.parse = querystring; + +},{}],22:[function(require,module,exports){ +'use strict'; + +/** + * Check if we're required to add a port number. + * + * @see https://url.spec.whatwg.org/#default-port + * @param {Number|String} port Port number we need to check + * @param {String} protocol Protocol we need to check against. + * @returns {Boolean} Is it a default port for the given protocol + * @api private + */ +module.exports = function required(port, protocol) { + protocol = protocol.split(':')[0]; + port = +port; + + if (!port) return false; + + switch (protocol) { + case 'http': + case 'ws': + return port !== 80; + + case 'https': + case 'wss': + return port !== 443; + + case 'ftp': + return port !== 21; + + case 'gopher': + return port !== 70; + + case 'file': + return false; + } + + return port !== 0; +}; + +},{}],23:[function(require,module,exports){ +(function (global){(function (){ +'use strict'; + +var required = require('requires-port') + , qs = require('querystringify') + , controlOrWhitespace = /^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/ + , CRHTLF = /[\n\r\t]/g + , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\// + , port = /:\d+$/ + , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i + , windowsDriveLetter = /^[a-zA-Z]:/; + +/** + * Remove control characters and whitespace from the beginning of a string. + * + * @param {Object|String} str String to trim. + * @returns {String} A new string representing `str` stripped of control + * characters and whitespace from its beginning. + * @public + */ +function trimLeft(str) { + return (str ? str : '').toString().replace(controlOrWhitespace, ''); +} + +/** + * These are the parse rules for the URL parser, it informs the parser + * about: + * + * 0. The char it Needs to parse, if it's a string it should be done using + * indexOf, RegExp using exec and NaN means set as current value. + * 1. The property we should set when parsing this value. + * 2. Indication if it's backwards or forward parsing, when set as number it's + * the value of extra chars that should be split off. + * 3. Inherit from location if non existing in the parser. + * 4. `toLowerCase` the resulting value. + */ +var rules = [ + ['#', 'hash'], // Extract from the back. + ['?', 'query'], // Extract from the back. + function sanitize(address, url) { // Sanitize what is left of the address + return isSpecial(url.protocol) ? address.replace(/\\/g, '/') : address; + }, + ['/', 'pathname'], // Extract from the back. + ['@', 'auth', 1], // Extract from the front. + [NaN, 'host', undefined, 1, 1], // Set left over value. + [/:(\d*)$/, 'port', undefined, 1], // RegExp the back. + [NaN, 'hostname', undefined, 1, 1] // Set left over. +]; + +/** + * These properties should not be copied or inherited from. This is only needed + * for all non blob URL's as a blob URL does not include a hash, only the + * origin. + * + * @type {Object} + * @private + */ +var ignore = { hash: 1, query: 1 }; + +/** + * The location object differs when your code is loaded through a normal page, + * Worker or through a worker using a blob. And with the blobble begins the + * trouble as the location object will contain the URL of the blob, not the + * location of the page where our code is loaded in. The actual origin is + * encoded in the `pathname` so we can thankfully generate a good "default" + * location from it so we can generate proper relative URL's again. + * + * @param {Object|String} loc Optional default location object. + * @returns {Object} lolcation object. + * @public + */ +function lolcation(loc) { + var globalVar; + + if (typeof window !== 'undefined') globalVar = window; + else if (typeof global !== 'undefined') globalVar = global; + else if (typeof self !== 'undefined') globalVar = self; + else globalVar = {}; + + var location = globalVar.location || {}; + loc = loc || location; + + var finaldestination = {} + , type = typeof loc + , key; + + if ('blob:' === loc.protocol) { + finaldestination = new Url(unescape(loc.pathname), {}); + } else if ('string' === type) { + finaldestination = new Url(loc, {}); + for (key in ignore) delete finaldestination[key]; + } else if ('object' === type) { + for (key in loc) { + if (key in ignore) continue; + finaldestination[key] = loc[key]; + } + + if (finaldestination.slashes === undefined) { + finaldestination.slashes = slashes.test(loc.href); + } + } + + return finaldestination; +} + +/** + * Check whether a protocol scheme is special. + * + * @param {String} The protocol scheme of the URL + * @return {Boolean} `true` if the protocol scheme is special, else `false` + * @private + */ +function isSpecial(scheme) { + return ( + scheme === 'file:' || + scheme === 'ftp:' || + scheme === 'http:' || + scheme === 'https:' || + scheme === 'ws:' || + scheme === 'wss:' + ); +} + +/** + * @typedef ProtocolExtract + * @type Object + * @property {String} protocol Protocol matched in the URL, in lowercase. + * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`. + * @property {String} rest Rest of the URL that is not part of the protocol. + */ + +/** + * Extract protocol information from a URL with/without double slash ("//"). + * + * @param {String} address URL we want to extract from. + * @param {Object} location + * @return {ProtocolExtract} Extracted information. + * @private + */ +function extractProtocol(address, location) { + address = trimLeft(address); + address = address.replace(CRHTLF, ''); + location = location || {}; + + var match = protocolre.exec(address); + var protocol = match[1] ? match[1].toLowerCase() : ''; + var forwardSlashes = !!match[2]; + var otherSlashes = !!match[3]; + var slashesCount = 0; + var rest; + + if (forwardSlashes) { + if (otherSlashes) { + rest = match[2] + match[3] + match[4]; + slashesCount = match[2].length + match[3].length; + } else { + rest = match[2] + match[4]; + slashesCount = match[2].length; + } + } else { + if (otherSlashes) { + rest = match[3] + match[4]; + slashesCount = match[3].length; + } else { + rest = match[4] + } + } + + if (protocol === 'file:') { + if (slashesCount >= 2) { + rest = rest.slice(2); + } + } else if (isSpecial(protocol)) { + rest = match[4]; + } else if (protocol) { + if (forwardSlashes) { + rest = rest.slice(2); + } + } else if (slashesCount >= 2 && isSpecial(location.protocol)) { + rest = match[4]; + } + + return { + protocol: protocol, + slashes: forwardSlashes || isSpecial(protocol), + slashesCount: slashesCount, + rest: rest + }; +} + +/** + * Resolve a relative URL pathname against a base URL pathname. + * + * @param {String} relative Pathname of the relative URL. + * @param {String} base Pathname of the base URL. + * @return {String} Resolved pathname. + * @private + */ +function resolve(relative, base) { + if (relative === '') return base; + + var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/')) + , i = path.length + , last = path[i - 1] + , unshift = false + , up = 0; + + while (i--) { + if (path[i] === '.') { + path.splice(i, 1); + } else if (path[i] === '..') { + path.splice(i, 1); + up++; + } else if (up) { + if (i === 0) unshift = true; + path.splice(i, 1); + up--; + } + } + + if (unshift) path.unshift(''); + if (last === '.' || last === '..') path.push(''); + + return path.join('/'); +} + +/** + * The actual URL instance. Instead of returning an object we've opted-in to + * create an actual constructor as it's much more memory efficient and + * faster and it pleases my OCD. + * + * It is worth noting that we should not use `URL` as class name to prevent + * clashes with the global URL instance that got introduced in browsers. + * + * @constructor + * @param {String} address URL we want to parse. + * @param {Object|String} [location] Location defaults for relative paths. + * @param {Boolean|Function} [parser] Parser for the query string. + * @private + */ +function Url(address, location, parser) { + address = trimLeft(address); + address = address.replace(CRHTLF, ''); + + if (!(this instanceof Url)) { + return new Url(address, location, parser); + } + + var relative, extracted, parse, instruction, index, key + , instructions = rules.slice() + , type = typeof location + , url = this + , i = 0; + + // + // The following if statements allows this module two have compatibility with + // 2 different API: + // + // 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments + // where the boolean indicates that the query string should also be parsed. + // + // 2. The `URL` interface of the browser which accepts a URL, object as + // arguments. The supplied object will be used as default values / fall-back + // for relative paths. + // + if ('object' !== type && 'string' !== type) { + parser = location; + location = null; + } + + if (parser && 'function' !== typeof parser) parser = qs.parse; + + location = lolcation(location); + + // + // Extract protocol information before running the instructions. + // + extracted = extractProtocol(address || '', location); + relative = !extracted.protocol && !extracted.slashes; + url.slashes = extracted.slashes || relative && location.slashes; + url.protocol = extracted.protocol || location.protocol || ''; + address = extracted.rest; + + // + // When the authority component is absent the URL starts with a path + // component. + // + if ( + extracted.protocol === 'file:' && ( + extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) || + (!extracted.slashes && + (extracted.protocol || + extracted.slashesCount < 2 || + !isSpecial(url.protocol))) + ) { + instructions[3] = [/(.*)/, 'pathname']; + } + + for (; i < instructions.length; i++) { + instruction = instructions[i]; + + if (typeof instruction === 'function') { + address = instruction(address, url); + continue; + } + + parse = instruction[0]; + key = instruction[1]; + + if (parse !== parse) { + url[key] = address; + } else if ('string' === typeof parse) { + index = parse === '@' + ? address.lastIndexOf(parse) + : address.indexOf(parse); + + if (~index) { + if ('number' === typeof instruction[2]) { + url[key] = address.slice(0, index); + address = address.slice(index + instruction[2]); + } else { + url[key] = address.slice(index); + address = address.slice(0, index); + } + } + } else if ((index = parse.exec(address))) { + url[key] = index[1]; + address = address.slice(0, index.index); + } + + url[key] = url[key] || ( + relative && instruction[3] ? location[key] || '' : '' + ); + + // + // Hostname, host and protocol should be lowercased so they can be used to + // create a proper `origin`. + // + if (instruction[4]) url[key] = url[key].toLowerCase(); + } + + // + // Also parse the supplied query string in to an object. If we're supplied + // with a custom parser as function use that instead of the default build-in + // parser. + // + if (parser) url.query = parser(url.query); + + // + // If the URL is relative, resolve the pathname against the base URL. + // + if ( + relative + && location.slashes + && url.pathname.charAt(0) !== '/' + && (url.pathname !== '' || location.pathname !== '') + ) { + url.pathname = resolve(url.pathname, location.pathname); + } + + // + // Default to a / for pathname if none exists. This normalizes the URL + // to always have a / + // + if (url.pathname.charAt(0) !== '/' && isSpecial(url.protocol)) { + url.pathname = '/' + url.pathname; + } + + // + // We should not add port numbers if they are already the default port number + // for a given protocol. As the host also contains the port number we're going + // override it with the hostname which contains no port number. + // + if (!required(url.port, url.protocol)) { + url.host = url.hostname; + url.port = ''; + } + + // + // Parse down the `auth` for the username and password. + // + url.username = url.password = ''; + + if (url.auth) { + index = url.auth.indexOf(':'); + + if (~index) { + url.username = url.auth.slice(0, index); + url.username = encodeURIComponent(decodeURIComponent(url.username)); + + url.password = url.auth.slice(index + 1); + url.password = encodeURIComponent(decodeURIComponent(url.password)) + } else { + url.username = encodeURIComponent(decodeURIComponent(url.auth)); + } + + url.auth = url.password ? url.username +':'+ url.password : url.username; + } + + url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host + ? url.protocol +'//'+ url.host + : 'null'; + + // + // The href is just the compiled result. + // + url.href = url.toString(); +} + +/** + * This is convenience method for changing properties in the URL instance to + * insure that they all propagate correctly. + * + * @param {String} part Property we need to adjust. + * @param {Mixed} value The newly assigned value. + * @param {Boolean|Function} fn When setting the query, it will be the function + * used to parse the query. + * When setting the protocol, double slash will be + * removed from the final url if it is true. + * @returns {URL} URL instance for chaining. + * @public + */ +function set(part, value, fn) { + var url = this; + + switch (part) { + case 'query': + if ('string' === typeof value && value.length) { + value = (fn || qs.parse)(value); + } + + url[part] = value; + break; + + case 'port': + url[part] = value; + + if (!required(value, url.protocol)) { + url.host = url.hostname; + url[part] = ''; + } else if (value) { + url.host = url.hostname +':'+ value; + } + + break; + + case 'hostname': + url[part] = value; + + if (url.port) value += ':'+ url.port; + url.host = value; + break; + + case 'host': + url[part] = value; + + if (port.test(value)) { + value = value.split(':'); + url.port = value.pop(); + url.hostname = value.join(':'); + } else { + url.hostname = value; + url.port = ''; + } + + break; + + case 'protocol': + url.protocol = value.toLowerCase(); + url.slashes = !fn; + break; + + case 'pathname': + case 'hash': + if (value) { + var char = part === 'pathname' ? '/' : '#'; + url[part] = value.charAt(0) !== char ? char + value : value; + } else { + url[part] = value; + } + break; + + case 'username': + case 'password': + url[part] = encodeURIComponent(value); + break; + + case 'auth': + var index = value.indexOf(':'); + + if (~index) { + url.username = value.slice(0, index); + url.username = encodeURIComponent(decodeURIComponent(url.username)); + + url.password = value.slice(index + 1); + url.password = encodeURIComponent(decodeURIComponent(url.password)); + } else { + url.username = encodeURIComponent(decodeURIComponent(value)); + } + } + + for (var i = 0; i < rules.length; i++) { + var ins = rules[i]; + + if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase(); + } + + url.auth = url.password ? url.username +':'+ url.password : url.username; + + url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host + ? url.protocol +'//'+ url.host + : 'null'; + + url.href = url.toString(); + + return url; +} + +/** + * Transform the properties back in to a valid and full URL string. + * + * @param {Function} stringify Optional query stringify function. + * @returns {String} Compiled version of the URL. + * @public + */ +function toString(stringify) { + if (!stringify || 'function' !== typeof stringify) stringify = qs.stringify; + + var query + , url = this + , host = url.host + , protocol = url.protocol; + + if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':'; + + var result = + protocol + + ((url.protocol && url.slashes) || isSpecial(url.protocol) ? '//' : ''); + + if (url.username) { + result += url.username; + if (url.password) result += ':'+ url.password; + result += '@'; + } else if (url.password) { + result += ':'+ url.password; + result += '@'; + } else if ( + url.protocol !== 'file:' && + isSpecial(url.protocol) && + !host && + url.pathname !== '/' + ) { + // + // Add back the empty userinfo, otherwise the original invalid URL + // might be transformed into a valid one with `url.pathname` as host. + // + result += '@'; + } + + // + // Trailing colon is removed from `url.host` when it is parsed. If it still + // ends with a colon, then add back the trailing colon that was removed. This + // prevents an invalid URL from being transformed into a valid one. + // + if (host[host.length - 1] === ':' || (port.test(url.hostname) && !url.port)) { + host += ':'; + } + + result += host + url.pathname; + + query = 'object' === typeof url.query ? stringify(url.query) : url.query; + if (query) result += '?' !== query.charAt(0) ? '?'+ query : query; + + if (url.hash) result += url.hash; + + return result; +} + +Url.prototype = { set: set, toString: toString }; + +// +// Expose the URL parser and some additional properties that might be useful for +// others or testing. +// +Url.extractProtocol = extractProtocol; +Url.location = lolcation; +Url.trimLeft = trimLeft; +Url.qs = qs; + +module.exports = Url; + +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"querystringify":21,"requires-port":22}]},{},[4])(4) +}); +//# sourceMappingURL=tus.js.map diff --git a/resources/views/dashboard.php b/resources/views/dashboard.php new file mode 100755 index 0000000..dc7d008 --- /dev/null +++ b/resources/views/dashboard.php @@ -0,0 +1,193 @@ + +
+ + + +
+
+

Storage Overview

+
+ Total: stats['storage']['total'] ?> + Used: stats['storage']['used'] ?> + Free: stats['storage']['free'] ?> +
+
+ + +
+
+ Overall Usage + stats['storage']['percent'] ?>% +
+
+
+
+
+ Used: stats['storage']['used'] ?> + Available: stats['storage']['free'] ?> + Total: stats['storage']['total'] ?> +
+
+
+ + +
+ stats['storage']['folders'] as $folder): ?> + +
+ 📁 +
+ +
+

+ +
+ +
+
+
+
+ % of total +
+
+
+ +
+ + + +
+

Storage Management

+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/resources/views/error.php b/resources/views/error.php new file mode 100755 index 0000000..e689e82 --- /dev/null +++ b/resources/views/error.php @@ -0,0 +1,21 @@ + + +
+
errorCode) ?>
+ +

+ message)) ?> +

+ + + +
\ No newline at end of file diff --git a/resources/views/folder.php b/resources/views/folder.php new file mode 100644 index 0000000..7b806f9 --- /dev/null +++ b/resources/views/folder.php @@ -0,0 +1,163 @@ + +
+ + +
+

Files

+
+ + + +
+ +
+
+
+ + +
+ files)): ?> +
+
📁
+

No files in this folder

+

Upload your first file to get started

+ +
+ +
+ files as $file): ?> +
+
+ +
+ '🖼️', + 'pdf' => '📕', + 'doc', 'docx' => '📄', + 'xls', 'xlsx' => '📊', + 'zip', 'rar', '7z', 'tar', 'gz' => '📦', + 'mp3', 'wav', 'ogg' => '🎵', + 'mp4', 'avi', 'mkv', 'mov' => '🎬', + default => '📄' + }; + echo $icon; + ?> +
+
+ +
+

+ +

+ +
+
+ Size: + +
+
+ Modified: + +
+
+
+ +
+ + 🤏 + +
+ + + + +
+
+
+ +
+ +
+
+ + + + + + + diff --git a/resources/views/headers/dashboard.php b/resources/views/headers/dashboard.php new file mode 100644 index 0000000..d3ea792 --- /dev/null +++ b/resources/views/headers/dashboard.php @@ -0,0 +1,26 @@ + +
+
+

Welcome to your cloud storage + username) ?> 👋

+

Manage your cloud storage efficiently

+
+
+
+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/resources/views/headers/folder.php b/resources/views/headers/folder.php new file mode 100644 index 0000000..f1bc118 --- /dev/null +++ b/resources/views/headers/folder.php @@ -0,0 +1,32 @@ + +
+
+

+ 📁 + title) ?> +

+
+ files) ?> files + + totalSize ?> + + Last updated: lastModified ?> +
+
+
+ title !== 'documents' && $page->title !== 'media'): ?> +
+ + +
+ + + + 👈Back to Dashboard + +
+
\ No newline at end of file diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php new file mode 100755 index 0000000..1aef632 --- /dev/null +++ b/resources/views/layouts/app.php @@ -0,0 +1,53 @@ +page; +?> + + + + + + <?= htmlspecialchars($page->title() ?? 'Cloud App') ?> + + + + + +layoutConfig->header === 'default'): ?> +
+ +
+ + layoutConfig->header . '.php'; + if (file_exists($headerFile)): + include $headerFile; + ?> + + + +
+ content ?> + +
+layoutConfig->showFooter): ?> + + + + + \ No newline at end of file diff --git a/resources/views/license.php b/resources/views/license.php new file mode 100644 index 0000000..7dbeda4 --- /dev/null +++ b/resources/views/license.php @@ -0,0 +1,498 @@ + + + + +
+
+
+

MIT License

+

+ Behind every piece of software lies the dedication, expertise, and passion of talented individuals. + We champion a license that embodies the spirit of freedom and openness. +

+
+ +
+

Quick Summary

+

+ At our core, we value the vibrant community of individuals who make each line of code possible. + With pride, we champion a license that is renowned as one of the most liberal in the industry, + empowering users to unleash their creativity, explore new possibilities, and shape their digital + experiences. +

+
+ +
+ +
+
+
+ ✅ +
+

Can

+
+
    +
  • You may use the work commercially.
  • +
  • You may make changes to the work.
  • +
  • You may distribute the compiled code and/or source.
  • +
  • You may incorporate the work into something that has a more restrictive + license. +
  • +
  • You may use the work for private use.
  • +
+
+ + +
+
+
+ ✍️ +
+

Must

+
+
    +
  • You must include the copyright notice in all copies or substantial uses + of the work. +
  • +
  • You must include the license notice in all copies or substantial uses of + the work. +
  • +
+
+ + +
+
+
+ ❌ +
+

Cannot

+
+
    +
  • The work is provided "as is". You may not hold the author liable.
  • +
+
+
+ +
+
+

MIT License

+

Full License Text

+
+
+ Copyright © Grudinin Andrew (din9xtr) + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + associated documentation files (the "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the + following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial + portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +
+
+
+
diff --git a/resources/views/login.php b/resources/views/login.php new file mode 100755 index 0000000..be644a9 --- /dev/null +++ b/resources/views/login.php @@ -0,0 +1,21 @@ + + diff --git a/src/App.php b/src/App.php new file mode 100755 index 0000000..83dfcfa --- /dev/null +++ b/src/App.php @@ -0,0 +1,86 @@ + + */ + private array $middlewares = []; + + public function __construct(Container $container) + { + $this->container = $container; + } + + /** + * @param string|callable|MiddlewareInterface ...$middleware + * @return self + */ + public function middleware(...$middleware): self + { + foreach ($middleware as $m) { + $this->middlewares[] = $m; + } + return $this; + } + + public function router(Router $router): self + { + $this->middlewares[] = fn(ServerRequestInterface $request, callable $handler) => $router->dispatch($request); + return $this; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function dispatch(): void + { + $request = $this->container->get(ServerRequestInterface::class); + + $resolver = fn($entry) => is_string($entry) + ? $this->container->get($entry) + : $entry; + + $pipeline = new Relay($this->middlewares, $resolver); + + $response = $pipeline->handle($request); + + http_response_code($response->getStatusCode()); + + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + header("$name: $value", false); + } + } + + $body = $response->getBody(); + + if ($body->isSeekable()) { + $body->rewind(); + } + + while (!$body->eof()) { + echo $body->read(8192); + flush(); + } + if ($body instanceof Stream) { + $meta = $body->getMetadata(); + if (!empty($meta['uri']) && str_ends_with($meta['uri'], '.zip')) { + @unlink($meta['uri']); + } + } + } +} \ No newline at end of file diff --git a/src/Container/Container.php b/src/Container/Container.php new file mode 100755 index 0000000..1f4c35c --- /dev/null +++ b/src/Container/Container.php @@ -0,0 +1,173 @@ + + */ + private array $definitions = []; + /** + * @var array + */ + private array $shared = []; + /** + * @var array + */ + private array $request = []; + + public function has(string $id): bool + { + return isset($this->definitions[$id]) || class_exists($id); + } + + /** + * @param string $id + * @param callable|object $concrete + * @return void + */ + public function singleton(string $id, callable|object $concrete): void + { + $this->define($id, $concrete, Scope::Shared); + } + + /** + * @param string $id + * @param callable $factory + * @return void + */ + public function request(string $id, callable $factory): void + { + $this->define($id, $factory, Scope::Request); + } + + /** + * @param string $id + * @param callable $factory + * @return void + */ + public function factory(string $id, callable $factory): void + { + $this->define($id, $factory, Scope::Factory); + } + + /** + * @param string $id + * @param callable|object $concrete + * @param Scope $scope + * @return void + */ + private function define(string $id, callable|object $concrete, Scope $scope): void + { + $factory = $concrete instanceof Closure + ? $concrete + : (is_callable($concrete) ? $concrete(...) : fn() => $concrete); + + $this->definitions[$id] = new Definition($factory, $scope); + } + + public function get(string $id) + { + if ($def = $this->definitions[$id] ?? null) { + + return match ($def->scope) { + Scope::Shared => $this->shared[$id] + ??= ($def->factory)($this), + + Scope::Request => $this->request[$id] + ??= ($def->factory)($this), + + Scope::Factory => ($def->factory)($this), + }; + } + + if (class_exists($id)) { + return $this->shared[$id] ??= $this->autowire($id); + } + + throw new NotFoundException("Service $id not found"); + } + + /** + * @template T + * @param class-string $class + * @return mixed + * @throws ContainerException + */ + private function autowire(string $class): mixed + { + try { + $ref = new ReflectionClass($class); + + if (!$ref->isInstantiable()) { + throw new ContainerException("Class $class is not instantiable"); + } + + $ctor = $ref->getConstructor(); + if ($ctor === null) { + return new $class; + } + + $deps = []; + foreach ($ctor->getParameters() as $param) { + + $type = $param->getType(); + + if ($type === null) { + throw new ContainerException( + "Cannot resolve parameter \${$param->getName()} of $class: no type specified" + ); + } + + if ($type instanceof ReflectionUnionType) { + throw new ContainerException( + "Cannot resolve parameter \${$param->getName()} of $class: union types not supported" + ); + } + + if (!$type instanceof ReflectionNamedType) { + throw new ContainerException( + "Cannot resolve parameter \${$param->getName()} of $class: intersection types not supported" + ); + } + + if ($type->isBuiltin()) { + throw new ContainerException( + "Cannot resolve parameter \${$param->getName()} of $class: built-in type '{$type->getName()}' not supported" + ); + } + + $typeName = $type->getName(); + + if (!class_exists($typeName) && !interface_exists($typeName)) { + throw new ContainerException( + "Cannot resolve parameter \${$param->getName()} of $class: type '$typeName' not found" + ); + } + + + $deps[] = $this->get($type->getName()); + } + + return $ref->newInstanceArgs($deps); + } catch (Throwable $e) { + throw new ContainerException("Reflection failed for $class", 0, $e); + } + } + + public function beginRequest(): void + { + $this->request = []; + } +} diff --git a/src/Container/Definition.php b/src/Container/Definition.php new file mode 100755 index 0000000..6c383bd --- /dev/null +++ b/src/Container/Definition.php @@ -0,0 +1,16 @@ + + */ + public function toArray(): array; + + public function template(): string; + + public function layout(): ?string; + + public function title(): ?string; +} \ No newline at end of file diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php new file mode 100755 index 0000000..c0d9b20 --- /dev/null +++ b/src/Controllers/AuthController.php @@ -0,0 +1,97 @@ +logger->info("Login form started"); + $error = $_SESSION['login_error'] ?? null; + unset($_SESSION['login_error']); + + return View::display(new LoginViewModel( + title: 'Login', + error: $error, + csrf: CsrfMiddleware::generateToken() + )); + } + + public function loginSubmit(ServerRequestInterface $request): Response + { + $data = (array)($request->getParsedBody() ?? []); + + $username = (string)($data['username'] ?? ''); + $password = (string)($data['password'] ?? ''); + + $ip = $request->getServerParams()['REMOTE_ADDR'] ?? null; + $ua = $request->getHeaderLine('User-Agent') ?: null; + + $this->logger->info('Login submitted', [ + 'username' => $username, + 'ip' => $ip, + ]); + + $authToken = $this->loginService->attemptLogin( + $username, + $password, + $ip, + $ua + ); + + if ($authToken !== null) { + session_regenerate_id(true); + + return new Response( + 302, + [ + 'Location' => '/', + 'Set-Cookie' => sprintf( + 'auth_token=%s; HttpOnly; SameSite=Strict; Path=/; Secure', + $authToken + ), + ] + ); + } + + $_SESSION['login_error'] = 'Invalid credentials'; + + return new Response(302, ['Location' => '/login']); + } + + public function logout(ServerRequestInterface $request): Response + { + $token = $request->getCookieParams()['auth_token'] ?? null; + + if ($token) { + $this->loginService->logout($token); + } + + session_destroy(); + + return new Response( + 302, + [ + 'Location' => '/login', + 'Set-Cookie' => + 'auth_token=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; HttpOnly' + ] + ); + } +} diff --git a/src/Controllers/DashboardController.php b/src/Controllers/DashboardController.php new file mode 100755 index 0000000..f516eb6 --- /dev/null +++ b/src/Controllers/DashboardController.php @@ -0,0 +1,86 @@ +getAttribute('user'); + + $this->userStorageInitializer->init($user->id); + + $storage = $this->storageService->getStats($user); + + $folders = []; + + foreach ($storage->byFolder as $name => $bytes) { + $percent = getStoragePercent( + $bytes, + $storage->totalBytes + ); + + $folders[] = [ + 'name' => $name, + 'size' => formatBytes($bytes), + 'percent' => $percent, + ]; + } + + $this->logger->info('Dashboard loaded successfully'); + + } catch (Throwable $exception) { + $this->logger->error($exception->getMessage(), ['exception' => $exception]); + return View::display(new DashboardViewModel( + title: 'Dashboard', + username: $user->username, + stats: [ + 'storage' => [ + 'total' => formatBytes(0), + 'used' => formatBytes(0), + 'free' => formatBytes(0), + 'percent' => 0, + 'folders' => [], + ], + ], + csrf: CsrfMiddleware::generateToken(), + )); + } + + return View::display(new DashboardViewModel( + title: 'Dashboard', + username: $user->username, + stats: [ + 'storage' => [ + 'total' => formatBytes($storage->totalBytes), + 'used' => formatBytes($storage->usedBytes), + 'free' => formatBytes($storage->freeBytes), + 'percent' => $storage->percent, + 'folders' => $folders, + ], + ], + csrf: CsrfMiddleware::generateToken(), + )); + } +} \ No newline at end of file diff --git a/src/Controllers/LicenseController.php b/src/Controllers/LicenseController.php new file mode 100644 index 0000000..74a4a48 --- /dev/null +++ b/src/Controllers/LicenseController.php @@ -0,0 +1,15 @@ +getAttribute('user'); + + /** @var array|null $data */ + $data = $request->getParsedBody(); + $name = trim($data['name'] ?? ''); + + if ($name === '' || !preg_match('/^[a-zA-Z\x{0400}-\x{04FF}0-9_\- ]+$/u', $name)) { + return new Response(302, ['Location' => '/']); + } + + + $this->storageService->createFolder($user, $name); + + /** @phpstan-ignore catch.neverThrown */ + } catch (Throwable $e) { + $this->logger->error('Failed to save folder: ' . $e->getMessage(), [ + 'user_id' => $user->id, + 'exception' => $e + ]); + return new Response(302, ['Location' => '/?error=save_failed']); + } + + return new Response(302, ['Location' => '/']); + } + + /** + * @throws JsonException + */ + public function uploadFile(ServerRequestInterface $request): Response + { + /** @var User $user */ + $user = $request->getAttribute('user'); + + /** @var array|null $data */ + $data = $request->getParsedBody(); + $folder = trim($data['folder'] ?? ''); + + $file = $request->getUploadedFiles()['file'] ?? null; + + if (!$file instanceof UploadedFileInterface) { + return $this->jsonError('no_file', 400); + } + + if ($file->getError() !== UPLOAD_ERR_OK) { + return $this->jsonError('upload_failed', 422); + } + + try { + $this->storageService->uploadFile($user, $folder, $file); + + $this->logger->info('File uploaded', [ + 'user_id' => $user->id, + 'folder' => $folder, + 'size' => $file->getSize(), + ]); + + return $this->jsonSuccess(['code' => 'file_uploaded']); + + } catch (Throwable $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return $this->jsonError('save_failed', 500); + } + } + + /** + * @param array $payload + * @throws JsonException + */ + private function jsonSuccess(array $payload = []): Response + { + return new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode(['success' => true] + $payload, JSON_THROW_ON_ERROR) + ); + } + + /** + * @throws JsonException + */ + private function jsonError(string $code, int $status): Response + { + return new Response( + $status, + ['Content-Type' => 'application/json'], + json_encode([ + 'success' => false, + 'error' => $code, + ], JSON_THROW_ON_ERROR) + ); + } + + public function showFolder(ServerRequestInterface $request, string $folder): string + { + $user = $request->getAttribute('user'); + $csrfToken = CsrfMiddleware::generateToken(); + + $folderPath = $this->storageService->getDriver()->getUserFolderPath($user->id, $folder); + + $files = []; + $totalBytes = 0; + $lastModified = null; + + if (is_dir($folderPath)) { + foreach (scandir($folderPath) as $entry) { + if (in_array($entry, ['.', '..'])) continue; + + $fullPath = $folderPath . '/' . $entry; + if (!is_file($fullPath)) continue; + + $size = filesize($fullPath); + $modifiedTime = filemtime($fullPath); + + if ($size === false || $modifiedTime === false) { + continue; + } + + $modified = date('Y-m-d H:i:s', $modifiedTime); + + $files[] = [ + 'name' => $entry, + 'size' => $this->humanFileSize($size), + 'modified' => $modified, + ]; + + $totalBytes += $size; + if ($lastModified === null || $modifiedTime > strtotime($lastModified)) { + $lastModified = $modified; + } + } + } + + return View::display(new FolderViewModel( + title: $folder, + files: $files, + csrf: $csrfToken, + totalSize: $this->humanFileSize($totalBytes), + lastModified: $lastModified ?? '—' + )); + } + + private function humanFileSize(int $bytes): string + { + $sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if ($bytes === 0) return '0 B'; + $factor = floor(log($bytes, 1024)); + return sprintf("%.2f %s", $bytes / (1024 ** $factor), $sizes[(int)$factor]); + } + + public function deleteFolder(ServerRequestInterface $request, string $folder): Response + { + /** @var User $user */ + $user = $request->getAttribute('user'); + + $this->storageService->deleteFolder($user, $folder); + + return new Response(302, ['Location' => '/']); + } + + public function downloadFile(ServerRequestInterface $request): Response + { + /** @var User $user */ + $user = $request->getAttribute('user'); + + $query = $request->getQueryParams(); + $folder = $query['folder'] ?? ''; + $file = $query['file'] ?? ''; + $this->logger->info('Downloading file', ['user_id' => $user->id, 'folder' => $folder, 'file' => $file, 'query' => $query]); + + try { + $path = $this->storageService->getFileForDownload($user, $folder, $file); + $this->logger->info('File downloaded', ['path' => $path]); + + $filename = basename($path); + + $mimeType = mime_content_type($path) ?: 'application/octet-stream'; + + $fileSize = filesize($path); + + $fileStream = fopen($path, 'rb'); + if ($fileStream === false) { + $this->logger->error('Cannot open file for streaming', ['path' => $path]); + return new Response(500); + } + $stream = Stream::create($fileStream); + + return new Response( + 200, + [ + 'Content-Type' => $mimeType, + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Length' => (string)$fileSize, + 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', + 'Pragma' => 'no-cache', + 'Expires' => '0', + 'Accept-Ranges' => 'bytes', + ], + $stream + ); + } catch (Throwable $e) { + $this->logger->warning('Download failed', [ + 'user_id' => $user->id, + 'file' => $file, + 'exception' => $e, + ]); + + return new Response(404); + } + } + + public function deleteFile(ServerRequestInterface $request): Response + { + /** @var User $user */ + $user = $request->getAttribute('user'); + + /** @var array $data */ + $data = $request->getParsedBody(); + + $folder = $data['folder'] ?? ''; + $file = $data['file_name'] ?? ''; + + $this->storageService->deleteFile($user, $folder, $file); + + $this->logger->info('File deleted', [ + 'user_id' => $user->id, + 'folder' => $folder, + 'file' => $file, + ]); + + return new Response( + 302, + ['Location' => '/folders/' . rawurlencode($folder)] + ); + + } + + /** + * @throws JsonException + * @throws RandomException + */ + public function downloadMultiple(ServerRequestInterface $request): Response + { + /** @var User $user */ + $user = $request->getAttribute('user'); + + $query = $request->getQueryParams(); + + $folder = $query['folder'] ?? ''; + $raw = $query['file_names'] ?? '[]'; + + $fileNames = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); + + if (!$fileNames || !is_array($fileNames)) { + return new Response(400); + } + + $zipPath = $this->storageService->buildZipForDownload( + user: $user, + folder: $folder, + files: $fileNames + ); + if (!file_exists($zipPath)) { + return new Response(404); + } + + $fileStream = @fopen($zipPath, 'rb'); + if ($fileStream === false) { + $this->logger->error('Cannot open zip file for streaming', ['path' => $zipPath]); + return new Response(500); + } + $stream = Stream::create($fileStream); + + return new Response( + 200, + [ + 'Content-Type' => 'application/zip', + 'Content-Disposition' => 'attachment; filename="files.zip"', + 'Content-Length' => (string)filesize($zipPath), + 'Cache-Control' => 'no-store', + ], + $stream + ); + } + + /** + * @throws JsonException + */ + public function deleteMultiple(ServerRequestInterface $request): Response + { + /** @var User $user */ + $user = $request->getAttribute('user'); + + $data = $request->getParsedBody(); + + if (!is_array($data)) { + return new Response(400); + } + + $folder = $data['folder'] ?? ''; + $raw = $data['file_names'] ?? '[]'; + + $files = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); + + $this->storageService->deleteMultipleFiles($user, $folder, $files); + + return new Response( + 302, + ['Location' => '/folders/' . rawurlencode($folder)] + ); + } + +} diff --git a/src/Controllers/StorageTusController.php b/src/Controllers/StorageTusController.php new file mode 100644 index 0000000..96e660c --- /dev/null +++ b/src/Controllers/StorageTusController.php @@ -0,0 +1,152 @@ +getAttribute('user'); + if (!$user) { + return new Response(401); + } + + return match ($request->getMethod()) { + 'POST' => $this->create($request, $user), + 'OPTIONS' => $this->options(), + default => new Response(405), + }; + } + + /** @noinspection PhpUnhandledExceptionInspection */ + private function create(ServerRequestInterface $request, User $user): Response + { + $length = (int)$request->getHeaderLine('Upload-Length'); + if ($length <= 0) { + return new Response(400); + } + + $metadata = $this->parseMetadata( + $request->getHeaderLine('Upload-Metadata') + ); + + $uploadId = bin2hex(random_bytes(16)); + + $this->logger->info('CREATE_METADATA', $metadata); + + $this->storage->initTusUpload( + user: $user, + uploadId: $uploadId, + size: $length, + metadata: $metadata, + ); + $this->logger->debug('ID: ' . $uploadId); + return new Response( + 201, + [ + 'Tus-Resumable' => '1.0.0', + 'Location' => '/storage/tus/' . $uploadId, + ] + ); + } + + public function patch( + ServerRequestInterface $request, + ?string $id + ): Response + { + $user = $request->getAttribute('user'); + + + if (!$id) { + return new Response(404); + } + + $offset = (int)$request->getHeaderLine('Upload-Offset'); + $body = $request->getBody(); + + $written = $this->storage->writeTusChunk( + user: $user, + uploadId: $id, + offset: $offset, + stream: $body + ); + + return new Response( + 204, + [ + 'Tus-Resumable' => '1.0.0', + 'Upload-Offset' => (string)($offset + $written), + ] + ); + } + + public function head( + ServerRequestInterface $request, + ?string $id + ): Response + { + $user = $request->getAttribute('user'); + + if (!$id) { + return new Response(404); + } + + $status = $this->storage->getTusStatus($user, $id); + + return new Response( + 200, + [ + 'Tus-Resumable' => '1.0.0', + 'Upload-Offset' => (string)$status['offset'], + 'Upload-Length' => (string)$status['size'], + ] + ); + } + + /** + * @return array + */ + private function parseMetadata(string $raw): array + { + $result = []; + foreach (explode(',', $raw) as $item) { + if (!str_contains($item, ' ')) continue; + [$k, $v] = explode(' ', $item, 2); + $result[$k] = base64_decode($v); + } + return $result; + } + + private function options(): Response + { + return new Response( + 204, + [ + 'Tus-Resumable' => '1.0.0', + 'Tus-Version' => '1.0.0', + 'Tus-Extension' => 'creation,creation-defer-length', + 'Tus-Max-Size' => (string)(1024 ** 4), + 'Access-Control-Allow-Methods' => 'POST, PATCH, HEAD, OPTIONS', + 'Access-Control-Allow-Headers' => + 'Tus-Resumable, Upload-Length, Upload-Offset, Upload-Metadata, Content-Type', + ] + ); + } + +} \ No newline at end of file diff --git a/src/Helpers/helpers.php b/src/Helpers/helpers.php new file mode 100644 index 0000000..d4cb7d2 --- /dev/null +++ b/src/Helpers/helpers.php @@ -0,0 +1,23 @@ += 1024 && $i < count($units) - 1) { + $bytes /= 1024; + $i++; + } + + return round($bytes, 1) . ' ' . $units[$i]; + } +} +if (!function_exists('getStoragePercent')) { + function getStoragePercent(int $categoryBytes, int $totalBytes): float + { + return $totalBytes > 0 ? ($categoryBytes / $totalBytes * 100) : 0; + } +} diff --git a/src/Middlewares/AuthMiddleware.php b/src/Middlewares/AuthMiddleware.php new file mode 100755 index 0000000..7deccd3 --- /dev/null +++ b/src/Middlewares/AuthMiddleware.php @@ -0,0 +1,74 @@ + Path, no need to auth check + */ + private array $except = [ + '/login', + '/logout', + '/license' + ]; + + + public function __construct( + private readonly SessionRepository $sessions, + private readonly UserRepository $users, + ) + { + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface + { + $path = $request->getUri()->getPath(); + + $token = $request->getCookieParams()['auth_token'] ?? null; + $session = null; + + if ($token) { + $session = $this->sessions->findActiveByToken($token); + } + if ($path === '/login' && $session !== null) { + return new Response(302, ['Location' => '/']); + } + + if (in_array($path, $this->except, true)) { + return $handler->handle($request); + } + if (!$token) { + if ($request->getMethod() !== 'GET') { + return new Response(401); + } + return new Response(302, ['Location' => '/login']); + } + if ($session === null) { + return new Response( + 302, + [ + 'Location' => '/login', + 'Set-Cookie' => + 'auth_token=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/' + ] + ); + } + + $request = $request->withAttribute('user', $this->users->findById($session->userId)); + $request = $request->withAttribute('session', $session); + return $handler->handle($request); + } +} diff --git a/src/Middlewares/CsrfMiddleware.php b/src/Middlewares/CsrfMiddleware.php new file mode 100755 index 0000000..5feebb8 --- /dev/null +++ b/src/Middlewares/CsrfMiddleware.php @@ -0,0 +1,73 @@ +getMethod(); + $path = $request->getUri()->getPath(); + + if ($this->isExcludedPath($path)) { + return $handler->handle($request); + } + + if (in_array($method, self::UNSAFE_METHODS, true)) { + + $token = $_POST['_csrf'] ?? ''; + if (!isset($_SESSION['_csrf']) || $token !== $_SESSION['_csrf']) { + return new Psr17Factory()->createResponse(403) + ->withBody(new Psr17Factory()->createStream('CSRF validation failed')); + } + } + + return $handler->handle($request); + } + + private function isExcludedPath(string $path): bool + { + if (str_starts_with($path, '/storage/tus')) { + return true; + } + + if (str_starts_with($path, '/api/')) { + return true; + } + + if (str_starts_with($path, '/webhook/')) { + return true; + } + + return false; + } + + public static function generateToken(): string + { + if (empty($_SESSION['_csrf']) || $_SESSION['_csrf_expire'] < time()) { + try { + $_SESSION['_csrf'] = bin2hex(random_bytes(32)); + $_SESSION['_csrf_expire'] = time() + 3600; + } catch (RandomException $e) { + throw new RuntimeException( + 'Failed to generate CSRF token: ' . $e->getMessage(), + 0, + $e + ); + } + } + + return $_SESSION['_csrf']; + } +} diff --git a/src/Middlewares/ThrottleMiddleware.php b/src/Middlewares/ThrottleMiddleware.php new file mode 100755 index 0000000..4940745 --- /dev/null +++ b/src/Middlewares/ThrottleMiddleware.php @@ -0,0 +1,66 @@ +getHeaderLine('X-Forwarded-For') ?: $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'; + $ip = explode(',', $ip)[0]; + + $stmt = $this->db->prepare("SELECT * FROM login_throttle WHERE ip = :ip ORDER BY id DESC LIMIT 1"); + $stmt->execute(['ip' => $ip]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + $now = time(); + $blockedUntil = null; + + if ($row) { + if ($row['attempts'] >= $this->maxAttempts && ($now - $row['last_attempt']) < $this->lockTime) { + $blockedUntil = $row['last_attempt'] + $this->lockTime; + } + } + + if ($blockedUntil && $now < $blockedUntil) { + return new Response(429, [], 'Too Many Requests'); + } + + $response = $handler->handle($request); + + if ($request->getUri()->getPath() === '/login') { + $attempts = ($row['attempts'] ?? 0); + if ($response->getStatusCode() === 302) { + $this->db->prepare(" + INSERT INTO login_throttle (ip, attempts, last_attempt) + VALUES (:ip, 0, :last_attempt) + ")->execute(['ip' => $ip, 'last_attempt' => $now]); + } else { + $attempts++; + $this->db->prepare(" + INSERT INTO login_throttle (ip, attempts, last_attempt) + VALUES (:ip, :attempts, :last_attempt) + ")->execute([ + 'ip' => $ip, + 'attempts' => $attempts, + 'last_attempt' => $now + ]); + } + } + return $response; + } +} diff --git a/src/Models/Session.php b/src/Models/Session.php new file mode 100755 index 0000000..3c60ae3 --- /dev/null +++ b/src/Models/Session.php @@ -0,0 +1,19 @@ +id; + } + set (int $id) { + $this->id = $id; + } + } + + public string $username { + get { + return $this->username; + } + set (string $username) { + $this->username = $username; + } + } + + public string $passwordHash { + get { + return $this->passwordHash; + } + set (string $passwordHash) { + $this->passwordHash = $passwordHash; + } + } + + public int $createdAt { + get { + return $this->createdAt; + } + set (int $createdAt) { + $this->createdAt = $createdAt; + } + } + + public function __construct( + int $id, + string $username, + string $passwordHash, + int $createdAt, + ) + { + $this->id = $id; + $this->username = $username; + $this->passwordHash = $passwordHash; + $this->createdAt = $createdAt; + } + + public function verifyPassword(string $password): bool + { + return password_verify($password, $this->passwordHash); + } +} \ No newline at end of file diff --git a/src/Repositories/Exceptions/RepositoryException.php b/src/Repositories/Exceptions/RepositoryException.php new file mode 100755 index 0000000..29569a3 --- /dev/null +++ b/src/Repositories/Exceptions/RepositoryException.php @@ -0,0 +1,10 @@ +db->prepare(" + INSERT INTO sessions ( + id, user_id, auth_token, ip, user_agent, + created_at, last_activity_at + ) VALUES ( + :id, :user_id, :auth_token, :ip, :user_agent, + :created_at, :last_activity_at + ) + "); + + $stmt->execute([ + 'id' => $id, + 'user_id' => $userId, + 'auth_token' => $token, + 'ip' => $ip, + 'user_agent' => $userAgent, + 'created_at' => $now, + 'last_activity_at' => $now, + ]); + + return new Session( + $id, + $userId, + $token, + $ip, + $userAgent, + $now, + $now, + null + ); + } catch (PDOException $e) { + $this->logger->critical('Failed to create session', [ + 'user_id' => $userId, + 'ip' => $ip, + 'exception' => $e, + ]); + + throw new RepositoryException( + 'Failed to create session', + previous: $e + ); + } catch (RandomException $e) { + $this->logger->critical('Failed to create session', [ + 'user_id' => $userId, + 'ip' => $ip, + 'exception' => $e, + ]); + throw new RepositoryException( + 'Failed to revoke session', + previous: $e + ); + } + } + + public function revokeByToken(string $token): void + { + try { + $stmt = $this->db->prepare(" + UPDATE sessions + SET revoked_at = :revoked_at + WHERE auth_token = :token AND revoked_at IS NULL + "); + + $stmt->execute([ + 'token' => $token, + 'revoked_at' => time(), + ]); + } catch (PDOException $e) { + $this->logger->error('Failed to revoke session', [ + 'token' => $token, + 'exception' => $e, + ]); + + throw new RepositoryException( + 'Failed to revoke session', + previous: $e + ); + } + } + + public function findActiveByToken(string $token): ?Session + { + try { + $stmt = $this->db->prepare(" + SELECT * + FROM sessions + WHERE auth_token = :token + AND revoked_at IS NULL + LIMIT 1 + "); + + $stmt->execute(['token' => $token]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row === false) { + return null; + } + + return new Session( + $row['id'], + (int)$row['user_id'], + $row['auth_token'], + $row['ip'], + $row['user_agent'], + (int)$row['created_at'], + (int)$row['last_activity_at'], + $row['revoked_at'] !== null ? (int)$row['revoked_at'] : null + ); + } catch (Throwable $e) { + $this->logger->error('Failed to fetch session by token', [ + 'exception' => $e, + ]); + + throw new RepositoryException( + 'Failed to fetch session', + previous: $e + ); + } + } +} diff --git a/src/Repositories/UserRepository.php b/src/Repositories/UserRepository.php new file mode 100755 index 0000000..e091f59 --- /dev/null +++ b/src/Repositories/UserRepository.php @@ -0,0 +1,80 @@ + $criteria + * @return User|null + */ + public function findBy(array $criteria): ?User + { + try { + if (empty($criteria)) { + throw new \InvalidArgumentException('Criteria cannot be empty'); + } + + $whereParts = []; + $params = []; + + foreach ($criteria as $field => $value) { + $whereParts[] = "$field = :$field"; + $params[$field] = $value; + } + + $whereClause = implode(' AND ', $whereParts); + $sql = "SELECT * FROM users WHERE $whereClause LIMIT 1"; + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row === false) { + return null; + } + + return new User( + (int)$row['id'], + $row['username'], + $row['password'], + (int)$row['created_at'] + ); + } catch (PDOException $e) { + $this->logger->error('Failed to fetch user by criteria', [ + 'criteria' => $criteria, + 'exception' => $e, + ]); + + throw new RepositoryException( + 'Failed to fetch user', + previous: $e + ); + } + } + + public function findByUsername(string $username): ?User + { + return $this->findBy(['username' => $username]); + } + + public function findById(int $id): ?User + { + return $this->findBy(['id' => $id]); + } + +} diff --git a/src/Router.php b/src/Router.php new file mode 100755 index 0000000..1e8508b --- /dev/null +++ b/src/Router.php @@ -0,0 +1,238 @@ +> + */ + private array $routeMiddlewares = []; + + public function __construct( + callable $routes, + private readonly ContainerInterface $container + ) + { + $this->dispatcher = simpleDispatcher($routes); + } + + public function middlewareFor(string $path, string|callable|MiddlewareInterface ...$middlewares): self + { + /** @var list $middlewares */ + + $this->routeMiddlewares[$path] = $middlewares; + return $this; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function dispatch(ServerRequestInterface $request): ResponseInterface + { + try { + $routeInfo = $this->dispatcher->dispatch( + $request->getMethod(), + rawurldecode($request->getUri()->getPath()) + ); + return match ($routeInfo[0]) { + Dispatcher::NOT_FOUND => $this->createErrorResponse(404, '404 Not Found'), + Dispatcher::METHOD_NOT_ALLOWED => $this->createErrorResponse(405, '405 Method Not Allowed'), + Dispatcher::FOUND => $this->handleFoundRoute($request, $routeInfo[1], $routeInfo[2]), + }; + } catch (Throwable $e) { + + if ($e instanceof NotFoundExceptionInterface) { + throw new $e; + } + + if ($e instanceof ContainerExceptionInterface) { + throw new $e; + } + + return $this->handleException($e); + } + } + + /** + * @param string|callable|array{0: class-string, 1: string} $handler + * @param array $routeParams + */ + private function handleFoundRoute( + ServerRequestInterface $request, + mixed $handler, + array $routeParams + ): ResponseInterface + { + foreach ($routeParams as $key => $value) { + $request = $request->withAttribute($key, $value); + } + $middlewares = $this->getMiddlewaresFor($request); + + $middlewares[] = fn(ServerRequestInterface $req, $next) => $this->ensureResponse($this->callHandler($req, $handler, $routeParams)); + + + $resolver = fn($entry) => is_string($entry) ? $this->container->get($entry) : $entry; + + $pipeline = new Relay($middlewares, $resolver); + + return $pipeline->handle($request); + } + + /** + * @return list + */ + private function getMiddlewaresFor(ServerRequestInterface $request): array + { + $path = $request->getUri()->getPath(); + + return $this->routeMiddlewares[$path] ?? []; + } + + /** + * @param ServerRequestInterface $request + * @param mixed $handler + * @param array $routeParams + * @return mixed + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RuntimeException + */ + private function callHandler(ServerRequestInterface $request, mixed $handler, array $routeParams = []): mixed + { + if (is_callable($handler)) { + return $handler($request); + } + + if (is_array($handler) && count($handler) === 2) { + [$controllerClass, $method] = $handler; + + if (!is_string($controllerClass)) { + throw new RuntimeException('Controller must be class-string'); + } + + $controller = $this->container->get($controllerClass); + + if (!method_exists($controller, $method)) { + throw new RuntimeException("Method $method not found in $controllerClass"); + } + + return $controller->$method($request, ...array_values($routeParams)); + } + + throw new RuntimeException('Invalid route handler'); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function ensureResponse(mixed $result): ResponseInterface + { + if ($result instanceof ResponseInterface) { + return $result; + } + + if (is_string($result)) { + return $this->createHtmlResponse($result); + } + + throw new RuntimeException('Handler must return string or ResponseInterface'); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function createHtmlResponse(string $html): ResponseInterface + { + $response = $this->psr17()->createResponse(200) + ->withHeader('Content-Type', 'text/html; charset=utf-8'); + $response->getBody()->write($html); + return $response; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function handleException(Throwable $e): ResponseInterface + { + + $this->container->get(LoggerInterface::class)->error('Unhandled exception', [ + 'exception' => $e, + ]); + + return $this->createErrorResponse(500, 'Internal Server Error'); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function createErrorResponse( + int $statusCode, + string $message, + ): ResponseInterface + { + $errorMessages = [ + 404 => [ + 'title' => '404 - Page Not Found', + 'message' => 'The page you are looking for might have been removed, or is temporarily unavailable.', + ], + 405 => [ + 'title' => '405 - Method Not Allowed', + 'message' => 'The requested method is not allowed for this resource.', + ], + 500 => [ + 'title' => '500 - Internal Server Error', + 'message' => 'Something went wrong on our server.', + ] + ]; + + $errorConfig = $errorMessages[$statusCode] ?? [ + 'title' => "$statusCode - Error", + 'message' => $message, + ]; + + $errorViewModel = new ErrorViewModel( + title: $errorConfig['title'], + errorCode: (string)$statusCode, + message: $errorConfig['message'], + ); + + $html = View::display($errorViewModel); + + return $this->psr17()->createResponse($statusCode) + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody($this->psr17()->createStream($html)); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function psr17(): Psr17Factory + { + return $this->container->get(Psr17Factory::class); + } +} diff --git a/src/Services/LoginService.php b/src/Services/LoginService.php new file mode 100755 index 0000000..7251fd7 --- /dev/null +++ b/src/Services/LoginService.php @@ -0,0 +1,48 @@ +users->findByUsername($username); + + if (!$user) { + return null; + } + + if (!$user->verifyPassword($password)) { + return null; + } + + $session = $this->sessions->create( + $user->id, + $ip, + $userAgent + ); + + return $session->authToken; + } + + public function logout(string $token): void + { + $this->sessions->revokeByToken($token); + } +} diff --git a/src/Storage/Drivers/LocalStorageDriver.php b/src/Storage/Drivers/LocalStorageDriver.php new file mode 100644 index 0000000..1480fc0 --- /dev/null +++ b/src/Storage/Drivers/LocalStorageDriver.php @@ -0,0 +1,356 @@ +defaultLimitBytes; + } + + public function getUsedBytes(int|string $userId): int + { + return array_sum($this->getUsedBytesByCategory($userId)); + } + + public function getUsedBytesByCategory(int|string $userId): array + { + $userPath = $this->basePath . '/users/' . $userId; + + if (!is_dir($userPath)) { + return []; + } + + $result = []; + + foreach (scandir($userPath) as $entry) { + if ($entry === '.' || $entry === '..' || $entry === '.tus') { + continue; + } + + $fullPath = $userPath . '/' . $entry; + + if (!is_dir($fullPath)) { + continue; + } + + $result[$entry] = $this->getDirectorySize($fullPath); + } + + return $result; + } + + private function getDirectorySize(string $path): int + { + if (!is_dir($path)) { + return 0; + } + + $size = 0; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + $size += $file->getSize(); + } + + return $size; + } + + public function createFolder(int|string $userId, string $name): void + { + $path = $this->basePath . '/users/' . $userId . '/' . $name; + + if (is_dir($path)) { + return; + } + + mkdir($path, 0755, true); + } + + public function getUserFolderPath(int|string $userId, string $folder): string + { + return $this->basePath . '/users/' . $userId . '/' . $folder; + } + + public function getUserPath(int|string $userId): string + { + return $this->basePath . '/users/' . $userId; + } + + public function storeUploadedFile( + int|string $userId, + string $folder, + UploadedFileInterface $file, + string $targetName + ): void + { + $dir = $this->getUserFolderPath($userId, $folder); + + if (!is_dir($dir)) { + throw new RuntimeException('Folder not found'); + } + + $file->moveTo($dir . '/' . $targetName); + } + + private function tusDir(int|string $userId): string + { + return $this->basePath . '/users/' . $userId . '/.tus'; + } + + private function tusMeta(int|string $userId, string $id): string + { + return $this->tusDir($userId) . "/$id.meta"; + } + + private function tusBin(int|string $userId, string $id): string + { + return $this->tusDir($userId) . "/$id.bin"; + } + + /** + * @throws JsonException + */ + public function tusInit( + int|string $userId, + string $uploadId, + int $size, + array $metadata + ): void + { + $dir = $this->tusDir($userId); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + + file_put_contents( + $this->tusMeta($userId, $uploadId), + json_encode([ + 'size' => $size, + 'offset' => 0, + 'metadata' => $metadata, + ], JSON_THROW_ON_ERROR) + ); + + touch($this->tusBin($userId, $uploadId)); + } + + /** + * @throws JsonException + */ + public function tusWriteChunk( + int|string $userId, + string $uploadId, + int $offset, + StreamInterface $stream + ): int + { + $metaFile = $this->tusMeta($userId, $uploadId); + $binFile = $this->tusBin($userId, $uploadId); + + if (!is_file($metaFile) || !is_file($binFile)) { + throw new RuntimeException('Upload not found'); + } + + $metaContent = file_get_contents($metaFile); + if ($metaContent === false) { + throw new RuntimeException('Failed to read TUS metadata'); + } + + $meta = json_decode( + $metaContent, + true, + 512, + JSON_THROW_ON_ERROR + ); + + if ($offset !== $meta['offset']) { + throw new RuntimeException('Invalid upload offset'); + } + + $fp = fopen($binFile, 'c+'); + if ($fp === false) { + throw new RuntimeException('Failed to open binary file for writing'); + } + + if (fseek($fp, $offset) !== 0) { + fclose($fp); + throw new RuntimeException('Failed to seek to offset'); + } + + $detachedStream = $stream->detach(); + if ($detachedStream === null) { + fclose($fp); + throw new RuntimeException('Stream is already detached'); + } + + $written = stream_copy_to_stream($detachedStream, $fp); + fclose($fp); + + if ($written === false) { + throw new RuntimeException('Failed to write chunk'); + } + + $meta['offset'] += $written; + + if ($meta['offset'] >= $meta['size']) { + $this->finalizeTusUpload($userId, $uploadId, $meta); + } else { + $updatedMeta = json_encode($meta, JSON_THROW_ON_ERROR); + if (file_put_contents($metaFile, $updatedMeta) === false) { + throw new RuntimeException('Failed to update TUS metadata'); + } + } + + return $written; + } + + /** + * Finalize TUS upload + * + * @param int|string $userId + * @param string $uploadId + * @param array{ + * size: int, + * offset: int, + * metadata: array, + * created_at: string, + * expires_at: string + * } $meta + * @throws RuntimeException + */ + private function finalizeTusUpload( + int|string $userId, + string $uploadId, + array $meta + ): void + { + $folder = $meta['metadata']['folder'] ?? 'default'; + $filename = $meta['metadata']['filename'] ?? $uploadId; + + $targetDir = $this->getUserFolderPath($userId, $folder); + if (!is_dir($targetDir)) { + if (!mkdir($targetDir, 0755, true) && !is_dir($targetDir)) { + throw new RuntimeException("Failed to create target directory: $targetDir"); + } + } + + $targetFile = $targetDir . '/' . $filename; + + if (file_exists($targetFile)) { + $filename = $this->addSuffixToFilename($filename, $targetDir); + $targetFile = $targetDir . '/' . $filename; + } + + $sourceFile = $this->tusBin($userId, $uploadId); + + if (!rename($sourceFile, $targetFile)) { + throw new RuntimeException("Failed to move file from $sourceFile to $targetFile"); + } + + $metaFile = $this->tusMeta($userId, $uploadId); + if (!unlink($metaFile)) { + throw new RuntimeException("Failed to delete metadata file: $metaFile"); + } + } + + private function addSuffixToFilename(string $filename, string $targetDir): string + { + $pathInfo = pathinfo($filename); + $name = $pathInfo['filename']; + $ext = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; + + $counter = 1; + + do { + $newFilename = $name . '_' . $counter . $ext; + $newPath = $targetDir . '/' . $newFilename; + $counter++; + } while (file_exists($newPath) && $counter < 100); + + return $newFilename; + } + + /** + * @throws JsonException + */ + public function tusGetStatus( + int|string $userId, + string $uploadId + ): array + { + $metaFile = $this->tusMeta($userId, $uploadId); + + if (!is_file($metaFile)) { + throw new RuntimeException('Upload not found'); + } + + $metaContent = file_get_contents($metaFile); + if ($metaContent === false) { + throw new RuntimeException('Failed to read TUS metadata'); + } + + $meta = json_decode( + $metaContent, + true, + 512, + JSON_THROW_ON_ERROR + ); + + return [ + 'size' => (int)($meta['size'] ?? 0), + 'offset' => (int)($meta['offset'] ?? 0), + 'metadata' => $meta['metadata'] ?? [], + 'created_at' => $meta['created_at'] ?? date('c'), + 'expires_at' => $meta['expires_at'] ?? null, + ]; + } + + public function getFilePath( + int|string $userId, + string $folder, + string $filename + ): string + { + $path = $this->getUserFolderPath($userId, $folder) . '/' . $filename; + + if (!is_file($path)) { + throw new RuntimeException('File not found'); + } + + return $path; + } + + public function deleteFile( + int|string $userId, + string $folder, + string $filename + ): void + { + $path = $this->getFilePath($userId, $folder, $filename); + + if (!unlink($path)) { + throw new RuntimeException('Failed to delete file'); + } + } +} diff --git a/src/Storage/Drivers/StorageDriverInterface.php b/src/Storage/Drivers/StorageDriverInterface.php new file mode 100644 index 0000000..3516ae7 --- /dev/null +++ b/src/Storage/Drivers/StorageDriverInterface.php @@ -0,0 +1,82 @@ + */ + public function getUsedBytesByCategory(int|string $userId): array; + + public function createFolder(int|string $userId, string $name): void; + + public function storeUploadedFile( + int|string $userId, + string $folder, + UploadedFileInterface $file, + string $targetName + ): void; + + public function getUserFolderPath(int|string $userId, string $folder): string; + + /** + * Initialize TUS upload + * + * @param int|string $userId + * @param string $uploadId + * @param int $size + * @param array $metadata folder + */ + public function tusInit( + int|string $userId, + string $uploadId, + int $size, + array $metadata + ): void; + + public function tusWriteChunk( + int|string $userId, + string $uploadId, + int $offset, + StreamInterface $stream + ): int; + + /** + * Get TUS upload status + * + * @param int|string $userId + * @param string $uploadId + * @return array{ + * size: int, + * offset: int, + * metadata: array, + * created_at: string, + * expires_at: string|null + * } + */ + public function tusGetStatus( + int|string $userId, + string $uploadId + ): array; + + public function getUserPath(int|string $userId): string; + + public function getFilePath( + int|string $userId, + string $folder, + string $filename + ): string; + + public function deleteFile( + int|string $userId, + string $folder, + string $filename + ): void; +} diff --git a/src/Storage/StorageGuard.php b/src/Storage/StorageGuard.php new file mode 100644 index 0000000..66c28cb --- /dev/null +++ b/src/Storage/StorageGuard.php @@ -0,0 +1,27 @@ +logger->warning("Physical disk is full", ['path' => $path]); + + throw new RuntimeException('Physical disk is full'); + } + } +} diff --git a/src/Storage/StorageService.php b/src/Storage/StorageService.php new file mode 100644 index 0000000..5cf661f --- /dev/null +++ b/src/Storage/StorageService.php @@ -0,0 +1,332 @@ +driver; + } + + public function getStats(User $user): StorageStats + { + $total = $this->driver->getTotalBytes($user->id); + $used = $this->driver->getUsedBytes($user->id); + $free = max(0, $total - $used); + $percent = $total > 0 + ? (int)round(($used / $total) * 100) + : 0; + + return new StorageStats( + totalBytes: $total, + usedBytes: $used, + freeBytes: $free, + percent: min(100, $percent), + byFolder: $this->driver->getUsedBytesByCategory($user->id) + ); + } + + public function createFolder(User $user, string $name): void + { + $this->driver->createFolder($user->id, $name); + } + + public function uploadFile( + User $user, + string $folder, + UploadedFileInterface $file + ): void + { + $this->assertValidFolderName($folder); + + $stats = $this->getStats($user); + $fileSize = (int)$file->getSize(); + + if ($stats->freeBytes < $fileSize) { + throw new RuntimeException('Storage limit exceeded'); + } + + $folderPath = $this->driver->getUserPath($user->id); + $this->guard->assertEnoughSpace($folderPath, $fileSize); + + $safeName = $this->generateSafeFilename( + $file->getClientFilename() ?? 'file', + $folderPath . '/' . $folder + ); + $this->driver->storeUploadedFile( + $user->id, + $folder, + $file, + $safeName + ); + } + + private function generateSafeFilename(string $original, string $folderPath): string + { + $original = $this->fixFileNameEncoding($original); + $name = pathinfo($original, PATHINFO_FILENAME); + $ext = pathinfo($original, PATHINFO_EXTENSION); + + $safe = preg_replace('/[\/:*?"<>|\\\\]/', '_', $name); + + if (empty($safe)) { + $safe = 'file'; + } + $baseName = $safe . ($ext ? '.' . $ext : ''); + $basePath = $folderPath . '/' . $baseName; + + if (!file_exists($basePath)) { + error_log("Returning: " . $baseName); + return $baseName; + } + + $counter = 1; + $maxAttempts = 100; + + do { + $newFilename = $safe . '_' . $counter . ($ext ? '.' . $ext : ''); + $newPath = $folderPath . '/' . $newFilename; + $counter++; + } while (file_exists($newPath) && $counter <= $maxAttempts); + + return $newFilename; + } + + private function assertValidFolderName(string $folder): void + { + if ($folder === '' || !preg_match('/^[a-zA-Z\x{0400}-\x{04FF}0-9_\- ]+$/u', $folder)) { + throw new RuntimeException('Invalid folder'); + } + } + + private function fixFileNameEncoding(string $filename): string + { + $detected = mb_detect_encoding($filename, ['UTF-8', 'Windows-1251', 'ISO-8859-5', 'KOI8-R'], true); + + if ($detected && $detected !== 'UTF-8') { + $converted = mb_convert_encoding($filename, 'UTF-8', $detected); + $filename = $converted !== false ? $converted : "unknown_file"; + } + + return $filename; + } + + /** + * Initialize TUS upload + * + * @param User $user + * @param string $uploadId + * @param int $size + * @param array $metadata folder + * @throws RuntimeException + */ + public function initTusUpload( + User $user, + string $uploadId, + int $size, + array $metadata + ): void + { + $stats = $this->getStats($user); + + $userFolderPath = $this->driver->getUserPath($user->id); + + $this->guard->assertEnoughSpace($userFolderPath, $size); + + if ($stats->freeBytes < $size) { + throw new RuntimeException('Storage limit exceeded'); + } + + $this->driver->tusInit( + userId: $user->id, + uploadId: $uploadId, + size: $size, + metadata: $metadata + ); + } + + /** + * Write TUS chunk + * + * @param User $user + * @param string $uploadId + * @param int $offset + * @param StreamInterface $stream + * @return int Number of bytes written + */ + public function writeTusChunk( + User $user, + string $uploadId, + int $offset, + StreamInterface $stream + ): int + { + return $this->driver->tusWriteChunk( + userId: $user->id, + uploadId: $uploadId, + offset: $offset, + stream: $stream + ); + } + + /** + * Get TUS upload status + * + * @param User $user + * @param string $uploadId + * @return array{ + * size: int, + * offset: int, + * metadata: array, + * created_at: string, + * expires_at: string|null + * } + */ + public function getTusStatus(User $user, string $uploadId): array + { + return $this->driver->tusGetStatus($user->id, $uploadId); + } + + public function deleteFile( + User $user, + string $folder, + string $filename + ): void + { + $this->assertValidFolderName($folder); + + if ($filename === '' || str_contains($filename, '..')) { + throw new RuntimeException('Invalid filename'); + } + + $this->driver->deleteFile($user->id, $folder, $filename); + } + + public function getFileForDownload( + User $user, + string $folder, + string $filename + ): string + { + $this->assertValidFolderName($folder); + + if ($filename === '' || str_contains($filename, '..')) { + throw new RuntimeException('Invalid filename'); + } + + return $this->driver->getFilePath($user->id, $folder, $filename); + } + + /** + * @param array $files + * @throws RandomException + */ + public function buildZipForDownload( + User $user, + string $folder, + array $files + ): string + { + $this->assertValidFolderName($folder); + + $tmpDir = sys_get_temp_dir() . '/cloud-' . $user->id; + if (!is_dir($tmpDir)) { + mkdir($tmpDir, 0700, true); + } + + $zipPath = $tmpDir . '/download-' . bin2hex(random_bytes(8)) . '.zip'; + + + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE) !== true) { + throw new RuntimeException('Cannot create zip'); + } + + foreach ($files as $file) { + if ($file === '' || str_contains($file, '..')) { + continue; + } + + $path = $this->driver->getFilePath($user->id, $folder, $file); + $zip->addFile($path, $file); + } + + $zip->close(); + + return $zipPath; + } + + /** + * @param array $files + */ + public function deleteMultipleFiles( + User $user, + string $folder, + array $files + ): void + { + $this->assertValidFolderName($folder); + + foreach ($files as $file) { + if ($file === '' || str_contains($file, '..')) { + continue; + } + + $path = $this->driver->getFilePath($user->id, $folder, $file); + @unlink($path); + } + } + + public function deleteFolder(User $user, string $folder): void + { + $this->assertValidFolderName($folder); + + $path = $this->driver->getUserFolderPath($user->id, $folder); + + if (!is_dir($path)) { + throw new RuntimeException("Folder not found: $folder"); + } + $this->deleteDirectoryRecursive($path); + } + + private function deleteDirectoryRecursive(string $dir): void + { + $items = scandir($dir); + if ($items === false) { + throw new RuntimeException("Failed to read directory: $dir"); + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') continue; + + $path = $dir . '/' . $item; + if (is_dir($path)) { + $this->deleteDirectoryRecursive($path); + } else { + @unlink($path); + } + } + + @rmdir($dir); + } + + +} diff --git a/src/Storage/StorageStats.php b/src/Storage/StorageStats.php new file mode 100644 index 0000000..5627597 --- /dev/null +++ b/src/Storage/StorageStats.php @@ -0,0 +1,18 @@ + */ + public array $byFolder = [], + ) + { + } +} diff --git a/src/Storage/UserStorageInitializer.php b/src/Storage/UserStorageInitializer.php new file mode 100644 index 0000000..be0167e --- /dev/null +++ b/src/Storage/UserStorageInitializer.php @@ -0,0 +1,40 @@ +basePath . '/users/' . $userId; + + foreach (self::CATEGORIES as $dir) { + $path = $userPath . '/' . $dir; + + if (!is_dir($path)) { + if (!mkdir($path, 0775, true)) { + throw new RuntimeException( + sprintf('can`t crate dir: %s', $path) + ); + } + } + } + } +} diff --git a/src/View.php b/src/View.php new file mode 100755 index 0000000..5d8a9a0 --- /dev/null +++ b/src/View.php @@ -0,0 +1,68 @@ +basePath = $basePath ?? '/var/www/resources/views/'; + } + + public function render(ViewModel $viewModel): string + { + $html = $this->renderTemplate($viewModel); + + if ($viewModel instanceof BaseViewModel && $viewModel->layout()) { + return $this->render( + new LayoutViewModel( + content: $html, + page: $viewModel + ) + ); + } + + return $html; + } + + private function renderTemplate(ViewModel $viewModel): string + { + $file = $this->basePath + . str_replace('.', '/', $viewModel->template()) + . '.php'; + + if (!file_exists($file)) { + throw new RuntimeException("Template not found: $file"); + } + + ob_start(); + + try { + include $file; + } catch (Throwable $e) { + ob_end_clean(); + throw new RuntimeException( + "Render template error $file: " . $e->getMessage(), + 0, + $e + ); + } + + return (string)ob_get_clean(); + } + + public static function display(ViewModel $vm): string + { + static $instance; + return ($instance ??= new self())->render($vm); + } +} diff --git a/src/ViewModels/BaseViewModel.php b/src/ViewModels/BaseViewModel.php new file mode 100755 index 0000000..042e7a6 --- /dev/null +++ b/src/ViewModels/BaseViewModel.php @@ -0,0 +1,45 @@ + + */ + public function toArray(): array + { + return [ + 'title' => $this->title, + 'layoutConfig' => $this->layoutConfig, + ...$this->data() + ]; + } + + public function layout(): ?string + { + return $this->layoutConfig->layout; + } + + /** + * @return array + */ + protected function data(): array + { + return []; + } + + abstract public function title(): string; + + abstract public function template(): string; +} \ No newline at end of file diff --git a/src/ViewModels/Dashboard/DashboardViewModel.php b/src/ViewModels/Dashboard/DashboardViewModel.php new file mode 100755 index 0000000..a16d5d5 --- /dev/null +++ b/src/ViewModels/Dashboard/DashboardViewModel.php @@ -0,0 +1,50 @@ + $stats + * @param string|null $csrf + */ + public function __construct( + string $title, + public string $username, + public array $stats = [], + public ?string $csrf = null, + + ) + { + $layoutConfig = new LayoutConfig( + header: 'dashboard', + showFooter: true, + ); + parent::__construct($layoutConfig, $title); + } + + public function title(): string + { + return $this->title; + } + + public function template(): string + { + return 'dashboard'; + } + + protected function data(): array + { + return [ + 'username' => $this->username, + 'stats' => $this->stats, + 'csrf' => $this->csrf, + ]; + } +} \ No newline at end of file diff --git a/src/ViewModels/Errors/ErrorViewModel.php b/src/ViewModels/Errors/ErrorViewModel.php new file mode 100755 index 0000000..1bd92d2 --- /dev/null +++ b/src/ViewModels/Errors/ErrorViewModel.php @@ -0,0 +1,42 @@ + $this->errorCode, + 'message' => $this->message, + ]; + } + + public function title(): string + { + return $this->title; + } +} \ No newline at end of file diff --git a/src/ViewModels/Folder/FolderViewModel.php b/src/ViewModels/Folder/FolderViewModel.php new file mode 100644 index 0000000..175d7a2 --- /dev/null +++ b/src/ViewModels/Folder/FolderViewModel.php @@ -0,0 +1,39 @@ + $files + */ + public function __construct( + string $title, + public array $files = [], + public ?string $csrf = null, + public ?string $totalSize = null, + public ?string $lastModified = null, + ) + { + $layoutConfig = new LayoutConfig( + header: 'folder', + showFooter: true, + ); + parent::__construct($layoutConfig, $title); + } + + public function title(): string + { + return $this->title; + } + + public function template(): string + { + return 'folder'; + } + +} \ No newline at end of file diff --git a/src/ViewModels/LayoutConfig.php b/src/ViewModels/LayoutConfig.php new file mode 100755 index 0000000..7c1f04e --- /dev/null +++ b/src/ViewModels/LayoutConfig.php @@ -0,0 +1,14 @@ +page->layout(); + + if ($layout === null) { + throw new RuntimeException( + 'LayoutViewModel requires page to have a layout, but layout() returned null' + ); + } + + return $layout; + } + + public function layout(): ?string + { + return null; + } + + public function toArray(): array + { + return []; + } + + public function title(): ?string + { + return null; + } +} diff --git a/src/ViewModels/LicenseViewModel.php b/src/ViewModels/LicenseViewModel.php new file mode 100644 index 0000000..7e295cd --- /dev/null +++ b/src/ViewModels/LicenseViewModel.php @@ -0,0 +1,31 @@ +title; + } + + public function template(): string + { + return 'license'; + } +} \ No newline at end of file diff --git a/src/ViewModels/Login/LoginViewModel.php b/src/ViewModels/Login/LoginViewModel.php new file mode 100755 index 0000000..a7acb13 --- /dev/null +++ b/src/ViewModels/Login/LoginViewModel.php @@ -0,0 +1,41 @@ + $this->error, + 'csrf' => $this->csrf, + ]; + } + + public function title(): string + { + return $this->title; + } +} \ No newline at end of file diff --git a/storage/.gitkeep b/storage/.gitkeep new file mode 100644 index 0000000..e69de29