mirror of
https://github.com/michalcz10/USB-RAID-Array.git
synced 2026-11-11 07:59:04 +00:00
Compare commits
4
Commits
d15fb27fd1
...
dc452059f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc452059f3 | ||
|
|
574662c1cc | ||
|
|
295a9aca07 | ||
|
|
565fd18379 |
@@ -0,0 +1,4 @@
|
|||||||
|
## Architecture
|
||||||
|
- Frontend: html + JavaScript
|
||||||
|
- Backend: PHP
|
||||||
|
- DB: MySQL with phpmyadmin
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# Nastavení databáze
|
||||||
|
|
||||||
|
Tento návod popisuje databázi webové aplikace USB RAID Array pro MySQL/MariaDB. Lze použít phpMyAdmin v XAMPP nebo příkazový klient `mysql`.
|
||||||
|
|
||||||
|
Vyber jednu ze dvou cest: [import SQL](#varianta-a-import-sql) nebo [ruční vytvoření tabulek](#varianta-b-ruční-vytvoření-tabulek). Poté pokračuj [vytvořením prvního správce](#první-administrátor) a [nastavením připojení](#připojení-aplikace).
|
||||||
|
|
||||||
|
## Co aplikace ukládá
|
||||||
|
|
||||||
|
| Tabulka | Účel | Používají ji |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `users` | Webové účty, hashe hesel, role, vzdálené cesty a oprávnění. | `login.php`, `adminpanel.php`, `changepassword.php` |
|
||||||
|
| `share` | Tokeny sdílených médií, cesta k souboru, expirace a povolení stažení. | `ftp/view.php`, `ftp/shared.php` |
|
||||||
|
|
||||||
|
Obsah souborů zůstává na SFTP serveru. Databáze uchovává jen účty a metadata sdílení.
|
||||||
|
|
||||||
|
### Původní SQL soubor a doplnění
|
||||||
|
|
||||||
|
[usbraidlogin.sql](usbraidlogin.sql) je původní dump z phpMyAdmin. Obsahuje vytvoření tabulky `users` a vložení uživatelských dat. **Neobsahuje `CREATE DATABASE`, `USE` ani tabulku `share`.** Tabulka `users` v tomto dumpu nemá primární ani unikátní klíč.
|
||||||
|
|
||||||
|
[share.sql](share.sql) doplňuje strukturu, kterou aktuální kód potřebuje pro sdílené odkazy. Je odvozená z dotazů v `view.php` a `shared.php`, nikoli z původního dumpu. Neobsahuje testovací odkazy ani uživatelská data. `IF NOT EXISTS` při opakovaném spuštění nevytvoří druhou tabulku, ale také neopraví již existující nekompatibilní schéma.
|
||||||
|
|
||||||
|
Obě varianty níže používají databázi `usbraidlogin`, InnoDB a `utf8mb4_unicode_ci` podle původního SQL. Import původního dumpu prováděj do nové databáze; na existující tabulce `users` by `CREATE TABLE` skončilo chybou.
|
||||||
|
|
||||||
|
## Varianta A: import SQL
|
||||||
|
|
||||||
|
### Přes phpMyAdmin
|
||||||
|
|
||||||
|
1. V XAMPP spusť Apache a MySQL.
|
||||||
|
2. Otevři `http://localhost/phpmyadmin/` a přihlas se databázovým účtem s právem vytvářet databáze a tabulky.
|
||||||
|
3. Na kartě **SQL** vytvoř databázi:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE `usbraidlogin`
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. V levém panelu vyber **usbraidlogin**. Tento krok je nutný, protože dump neobsahuje příkaz `USE`.
|
||||||
|
5. Otevři **Import**, vyber `Documentation/usbraidlogin.sql`, ponech formát SQL a spusť import.
|
||||||
|
6. Ve stejné databázi znovu otevři **Import** a importuj `Documentation/share.sql`.
|
||||||
|
7. Zkontroluj, že jsou přítomné tabulky `users` a `share`.
|
||||||
|
|
||||||
|
Původní dump vkládá i účty s hashi hesel. Z hashe nelze zjistit původní heslo. Pro vlastní instalaci vytvoř vlastní účet podle kapitoly [První administrátor](#první-administrátor). Importované účty před zpřístupněním instalace zkontroluj a ponech jen ty, které chceš používat.
|
||||||
|
|
||||||
|
### Přes příkazový řádek
|
||||||
|
|
||||||
|
V PowerShellu otevři klienta z XAMPP:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'C:\xampp\mysql\bin\mysql.exe' -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
Použij jméno svého správce databáze místo `root`, pokud se liší. Heslo se zadává až do výzvy klienta. Jestli má lokální účet prázdné heslo, potvrď výzvu Enterem.
|
||||||
|
|
||||||
|
Na Linuxu nebo pokud je klient v `PATH`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mysql -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
V klientovi spusť následující příkazy. Cesty uprav podle umístění repozitáře; v příkladu pro Windows jsou použité dopředné lomítka:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE `usbraidlogin`
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE `usbraidlogin`;
|
||||||
|
SOURCE C:/cesta/USB-RAID-Array/Documentation/usbraidlogin.sql;
|
||||||
|
SOURCE C:/cesta/USB-RAID-Array/Documentation/share.sql;
|
||||||
|
SHOW TABLES;
|
||||||
|
```
|
||||||
|
|
||||||
|
`SOURCE` je příkaz klienta `mysql`. V phpMyAdmin používej kartu Import. Na Linuxu nahraď cesty například `/cesta/USB-RAID-Array/Documentation/usbraidlogin.sql` a odpovídající cestou k `share.sql`.
|
||||||
|
|
||||||
|
## Varianta B: ruční vytvoření tabulek
|
||||||
|
|
||||||
|
Tato varianta vytvoří prázdné tabulky bez importovaných účtů. Následující SQL vlož do karty **SQL** v phpMyAdmin nebo do klienta `mysql`. Původní dump už v této variantě neimportuj.
|
||||||
|
|
||||||
|
### 1. Databáze
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE `usbraidlogin`
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE `usbraidlogin`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Pokud databáze už existuje, vyber ji příkazem `USE` a přeskoč `CREATE DATABASE`. Před vytvořením tabulek ověř jejich existenci pomocí `SHOW TABLES`; existující tabulky nemaž kvůli opakování návodu.
|
||||||
|
|
||||||
|
### 2. Tabulka `users`
|
||||||
|
|
||||||
|
Struktura přesně odpovídá tabulce v původním `usbraidlogin.sql`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`uname` varchar(255) NOT NULL,
|
||||||
|
`pswd` varchar(60) NOT NULL,
|
||||||
|
`admin` tinyint(1) NOT NULL DEFAULT '0',
|
||||||
|
`defPath` varchar(255) NOT NULL DEFAULT '/',
|
||||||
|
`delPer` tinyint(1) NOT NULL DEFAULT '0',
|
||||||
|
`downPer` tinyint(1) NOT NULL DEFAULT '0',
|
||||||
|
`upPer` tinyint(1) NOT NULL DEFAULT '0'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Sloupec | Význam |
|
||||||
|
| --- | --- |
|
||||||
|
| `uname` | Přihlašovací jméno. Každý účet vytvářej s odlišným jménem. |
|
||||||
|
| `pswd` | Bcrypt hash hesla vytvořený PHP, dlouhý 60 znaků. |
|
||||||
|
| `admin` | `1` pro správce, `0` pro běžný účet; správce má přístup ke správě účtů a statistikám serveru. |
|
||||||
|
| `defPath` | Absolutní vzdálená cesta na SFTP serveru, například `/mnt/raid`. |
|
||||||
|
| `delPer` | `1` povoluje mazání souborů a adresářů. |
|
||||||
|
| `downPer` | `1` povoluje stahování v rozhraní. |
|
||||||
|
| `upPer` | `1` povoluje nahrávání a úpravy, například vytváření, přejmenování, editaci a rozbalování archivů. |
|
||||||
|
|
||||||
|
Hodnoty oprávnění zadávej jako `0` nebo `1`. Role správce a souborová oprávnění jsou samostatné sloupce; pro plný přístup nastav prvnímu správci všechny čtyři přepínače na `1`.
|
||||||
|
|
||||||
|
Věrná kopie původního schématu nemá unikátní index pro `uname`. Administrace duplicity ověřuje při přidávání účtu, ale při ručním SQL je musíš zkontrolovat sám.
|
||||||
|
|
||||||
|
### 3. Tabulka `share`
|
||||||
|
|
||||||
|
Stejnou tabulku vytváří doplňující soubor `share.sql`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS `share` (
|
||||||
|
`token` char(32) NOT NULL,
|
||||||
|
`file_path` text NOT NULL,
|
||||||
|
`expiration` datetime NOT NULL,
|
||||||
|
`download_allowed` tinyint(1) NOT NULL DEFAULT '0',
|
||||||
|
PRIMARY KEY (`token`),
|
||||||
|
KEY `idx_share_expiration` (`expiration`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Sloupec | Význam |
|
||||||
|
| --- | --- |
|
||||||
|
| `token` | Identifikátor odkazu v URL; aplikace generuje 32 hexadecimálních znaků pomocí `bin2hex(random_bytes(16))`. |
|
||||||
|
| `file_path` | Vzdálená cesta ke sdílenému souboru. |
|
||||||
|
| `expiration` | Datum a čas konce platnosti. Dotaz v `shared.php` vyžaduje `expiration > NOW()`. |
|
||||||
|
| `download_allowed` | `1` povoluje stažení přes sdílený odkaz, `0` pouze náhled. |
|
||||||
|
|
||||||
|
Primární klíč zajišťuje jedinečnost tokenu, index `idx_share_expiration` usnadňuje dotazy podle expirace. Mezi tabulkami není cizí klíč: současný kód u odkazu neukládá autora ani ID uživatele.
|
||||||
|
|
||||||
|
### Alternativa: naklikání tabulek v phpMyAdmin
|
||||||
|
|
||||||
|
Místo spuštění uvedených `CREATE TABLE` můžeš stejné tabulky vytvořit přes formulář:
|
||||||
|
|
||||||
|
1. Vyber databázi `usbraidlogin`, zvol vytvoření nové tabulky `users` a počet sloupců **7**.
|
||||||
|
2. Zadej názvy a datové typy podle SQL výše: `uname VARCHAR(255)`, `pswd VARCHAR(60)`, `admin TINYINT(1)`, `defPath VARCHAR(255)`, `delPer TINYINT(1)`, `downPer TINYINT(1)` a `upPer TINYINT(1)`.
|
||||||
|
3. U všech sloupců ponech **NULL vypnuté** a nezapínej `A_I` (AUTO_INCREMENT). Přepínačům nastav výchozí hodnotu `0`, sloupci `defPath` hodnotu `/`. `uname` a `pswd` nemají výchozí hodnotu. Pro shodu s původním dumpem nenastavuj žádný primární klíč.
|
||||||
|
4. Vyber engine **InnoDB**, porovnávání **utf8mb4_unicode_ci** a tabulku ulož.
|
||||||
|
5. Vytvoř tabulku `share` se **4** sloupci: `token CHAR(32)`, `file_path TEXT`, `expiration DATETIME` a `download_allowed TINYINT(1)`.
|
||||||
|
6. Opět vypni NULL a AUTO_INCREMENT. U `download_allowed` nastav výchozí hodnotu `0`; ostatní sloupce výchozí hodnotu nemají. Sloupci `token` nastav index **PRIMARY**.
|
||||||
|
7. Použij InnoDB a utf8mb4_unicode_ci. Po uložení na kartě **Struktura** přidej běžný index `idx_share_expiration` na sloupci `expiration`.
|
||||||
|
8. Výsledek porovnej pomocí `SHOW CREATE TABLE users;` a `SHOW CREATE TABLE share;` s uvedenými SQL definicemi.
|
||||||
|
|
||||||
|
## První administrátor
|
||||||
|
|
||||||
|
### 1. Vytvoření hashe hesla
|
||||||
|
|
||||||
|
Heslo do `pswd` neukládej jako prostý text ani jako MD5/SHA hash. Přihlášení používá PHP `password_verify()` a aplikace vytváří hesla pomocí `PASSWORD_BCRYPT`.
|
||||||
|
|
||||||
|
Pokud je PHP v `PATH`, spusť:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
php -r "echo password_hash('ZVOLTE_VLASTNI_HESLO', PASSWORD_BCRYPT), PHP_EOL;"
|
||||||
|
```
|
||||||
|
|
||||||
|
V PowerShellu s XAMPP lze použít plnou cestu:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'C:\xampp\php\php.exe' -r "echo password_hash('ZVOLTE_VLASTNI_HESLO', PASSWORD_BCRYPT), PHP_EOL;"
|
||||||
|
```
|
||||||
|
|
||||||
|
`ZVOLTE_VLASTNI_HESLO` před spuštěním nahraď vlastním heslem. Zvol alespoň 8 znaků, jednu číslici a jedno velké písmeno, aby odpovídalo pravidlům správy účtů. Výstup bude bcrypt hash začínající například `$2y$10$`. Každé spuštění může pro stejné heslo vytvořit jiný platný hash.
|
||||||
|
|
||||||
|
Pokud nechceš heslo uvést v historii příkazové řádky, spusť `php -a` a zavolej `password_hash()` v interaktivním PHP shellu. Příkaz s vlastním heslem ani jeho výstup neukládej do veřejných ukázek dokumentace.
|
||||||
|
|
||||||
|
### 2. Vložení účtu
|
||||||
|
|
||||||
|
Nejprve ověř, zda zvolené jméno už není použité:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
USE `usbraidlogin`;
|
||||||
|
SELECT `uname`, `admin`, `defPath` FROM `users` WHERE `uname` = 'spravce';
|
||||||
|
```
|
||||||
|
|
||||||
|
Pokud dotaz nic nevrátí, vlož účet. Text `SEM_VLOZTE_BCRYPT_HASH` nahraď celým skutečným hashem z předchozího kroku a `/mnt/raid` skutečnou cestou na SFTP serveru:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO `users`
|
||||||
|
(`uname`, `pswd`, `admin`, `defPath`, `delPer`, `downPer`, `upPer`)
|
||||||
|
VALUES
|
||||||
|
('spravce', 'SEM_VLOZTE_BCRYPT_HASH', 1, '/mnt/raid', 1, 1, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Tento SQL příkaz je šablona — placeholder není platný hash. Do přihlašovacího formuláře následně zadávej jméno `spravce` a původní heslo, ze kterého jsi hash vytvořil. Další účty už lze přidávat přes **User management**.
|
||||||
|
|
||||||
|
Pokud jméno existuje, další stejné nevkládej. Pro obnovení přístupu ke svému účtu lze v phpMyAdmin nahradit pouze jeho hash:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE `users`
|
||||||
|
SET `pswd` = 'SEM_VLOZTE_BCRYPT_HASH'
|
||||||
|
WHERE `uname` = 'spravce';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Připojení aplikace
|
||||||
|
|
||||||
|
Pro aplikaci vytvoř samostatný databázový účet. Následující příklad předpokládá, že se PHP připojuje k databázi lokálně jako `usbraid_app` na hostiteli `localhost`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE USER 'usbraid_app'@'localhost' IDENTIFIED BY 'DOPLNTE_VLASTNI_HESLO_DB';
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON `usbraidlogin`.* TO 'usbraid_app'@'localhost';
|
||||||
|
```
|
||||||
|
|
||||||
|
DDL příkazy pro vytvoření tabulek spouštěj správcovským databázovým účtem. Aplikace pro běžný provoz nepotřebuje práva `CREATE` ani `DROP`. Pokud účet již existuje, `CREATE USER` neopakuj; nastav mu potřebná práva. Pro vzdálenou databázi použij hostitele odpovídajícího umístění PHP serveru místo `localhost`.
|
||||||
|
|
||||||
|
V existujícím `Web/config/config.php` nastav:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$DB_HOST = 'localhost';
|
||||||
|
$DB_USER = 'usbraid_app';
|
||||||
|
$DB_PASS = 'DOPLNTE_VLASTNI_HESLO_DB';
|
||||||
|
$DB_NAME = 'usbraidlogin';
|
||||||
|
```
|
||||||
|
|
||||||
|
Účet MySQL, webový účet z tabulky `users` a SFTP/SSH účet jsou tři různé věci. Změna hesla ve webové aplikaci mění pouze hash v `users.pswd`.
|
||||||
|
|
||||||
|
### Časové pásmo sdílených odkazů
|
||||||
|
|
||||||
|
`view.php` počítá expiraci v `Europe/Prague`, zatímco databázový dotaz ji porovnává s `NOW()`. Databázový čas musí odpovídat tomuto pásmu, jinak může odkaz vypršet dříve nebo později. Ověř:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT NOW() AS database_time,
|
||||||
|
@@global.time_zone AS global_time_zone,
|
||||||
|
@@session.time_zone AS session_time_zone,
|
||||||
|
@@system_time_zone AS system_time_zone;
|
||||||
|
```
|
||||||
|
|
||||||
|
Pokud databáze používá `SYSTEM`, ověř časové pásmo systému databázového serveru. U spravované instalace lze nakonfigurovat časové pásmo MySQL na `Europe/Prague`, pokud má server načtené tabulky časových pásem. Jednorázové `SET time_zone` v phpMyAdmin mění pouze jeho spojení a nevyřeší nastavení nových spojení aplikace. Pevný posun `+01:00` nepokrývá český letní čas.
|
||||||
|
|
||||||
|
## Ověření instalace
|
||||||
|
|
||||||
|
V databázi spusť:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
USE `usbraidlogin`;
|
||||||
|
SHOW TABLES;
|
||||||
|
DESCRIBE `users`;
|
||||||
|
DESCRIBE `share`;
|
||||||
|
|
||||||
|
SELECT `uname`, `admin`, `defPath`, `delPer`, `downPer`, `upPer`,
|
||||||
|
CHAR_LENGTH(`pswd`) AS hash_length
|
||||||
|
FROM `users`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Výsledek má obsahovat obě tabulky. U účtu vytvořeného podle návodu má mít `hash_length` hodnotu `60`. Délka sama o sobě ještě nepotvrzuje správnost hesla; tu ověří přihlášení.
|
||||||
|
|
||||||
|
V aplikaci pak ověř:
|
||||||
|
|
||||||
|
1. Přihlášení vlastním administrátorem a otevření správy účtů.
|
||||||
|
2. Správnou výchozí cestu a přístup k SFTP souborům.
|
||||||
|
3. Vytvoření sdíleného odkazu u testovacího média a jeho otevření v anonymním okně.
|
||||||
|
4. Platnost odkazu a chování volby stažení.
|
||||||
|
|
||||||
|
Změny role, cesty a oprávnění provedené přímo v databázi se do existujícího webového přihlášení nepromítnou okamžitě — hodnoty se načítají do session při přihlášení. Po takové změně se odhlas a znovu přihlas.
|
||||||
|
|
||||||
|
## Řešení chyb
|
||||||
|
|
||||||
|
| Chyba | Příčina a řešení |
|
||||||
|
| --- | --- |
|
||||||
|
| `Unknown database 'usbraidlogin'` | Vytvoř databázi a zkontroluj `$DB_NAME`. |
|
||||||
|
| `No database selected` při importu | Před importem vyber databázi v phpMyAdmin nebo spusť `USE usbraidlogin`. |
|
||||||
|
| `Table 'users' already exists` | Dump se importuje do již vytvořeného schématu. Zkontroluj existující tabulku a nepokračuj slepým mazáním dat. |
|
||||||
|
| `Table 'usbraidlogin.share' doesn't exist` | Importuj `share.sql` nebo tabulku vytvoř ručně podle návodu. |
|
||||||
|
| `Access denied for user` | Zkontroluj `$DB_USER`, `$DB_PASS`, hostitele a přidělená práva. |
|
||||||
|
| Přihlášení hlásí nesprávné údaje | Ověř jméno a bcrypt hash; do formuláře patří původní heslo. |
|
||||||
|
| Sdílený odkaz ihned expiruje | Zkontroluj časové pásmo a `NOW()` na databázovém serveru. |
|
||||||
|
| Čeština se ukládá nesprávně | Tabulky používají `utf8mb4`; současný helper v `config.php` nastavuje spojení na `utf8`. Pro plné čtyřbajtové znaky, například emoji, je potřeba sjednotit i spojení na `utf8mb4`. |
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Doplneni puvodniho usbraidlogin.sql o tabulku pro sdilene odkazy.
|
||||||
|
-- Nejprve vyberte databazi usbraidlogin v phpMyAdmin nebo prikazem USE.
|
||||||
|
-- Struktura odpovida INSERT/SELECT v Web/content/ftp/view.php a shared.php.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `share` (
|
||||||
|
`token` char(32) NOT NULL,
|
||||||
|
`file_path` text NOT NULL,
|
||||||
|
`expiration` datetime NOT NULL,
|
||||||
|
`download_allowed` tinyint(1) NOT NULL DEFAULT '0',
|
||||||
|
PRIMARY KEY (`token`),
|
||||||
|
KEY `idx_share_expiration` (`expiration`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -1,2 +1,155 @@
|
|||||||
# USB-RAID-pole
|
# USB RAID Array
|
||||||
Cílem práce je nalézt využití zastaralých paměťových médií jako například slabopaměťové USB flash disky s malou paměťovou kapacitou. Technologii RAID (NAS) pole v praxi implementujte na vícero RAID technologiích a vzájemně komparujte řešení. Komparujte chování navrženého systému minimálně na technologiích RAID 0, RAID 1 a RAID 5. Vytvořte vzdálené připojení na toto RAID pole s možností nahrání a stažení souborů po přihlášení uživatele (web login). K tomuto účelu vytvořte webovou aplikaci. Zabezpečte vzdálené připojení. Použité technologie: RAID, NAS, FTP, USB, Flash, HTML, PHP, MySQL.
|
|
||||||
|
Webový SFTP klient pro vzdálenou práci se soubory na RAID/NAS úložišti. Projekt vznikl jako školní práce zaměřená na využití starších USB flash disků a porovnání RAID 0, RAID 1 a RAID 5.
|
||||||
|
|
||||||
|
Aplikace běží na PHP serveru, účty a sdílené odkazy ukládá do MySQL/MariaDB a k úložišti se připojuje přes SFTP. Statistiky vzdáleného Linux serveru získává přes SSH. Samotné vytvoření RAID pole se provádí na serveru mimo tuto webovou aplikaci.
|
||||||
|
|
||||||
|
## Funkce
|
||||||
|
|
||||||
|
- Přihlášení, změna hesla a správa uživatelských účtů.
|
||||||
|
- Oprávnění pro nahrávání, stahování a mazání souborů.
|
||||||
|
- Procházení adresářů, hledání v aktuální složce a filtrování souborů/složek.
|
||||||
|
- Nahrávání souborů i adresářů, drag & drop a zobrazení průběhu přenosu.
|
||||||
|
- Vytváření, přejmenování a mazání položek; stažení adresáře jako ZIP.
|
||||||
|
- Editor textových souborů, náhled obrázků, videa, audia a PDF.
|
||||||
|
- Sdílení médií odkazem s expirací a volitelným stažením.
|
||||||
|
- Přehled disků, Linux MD RAID polí, CPU, RAM a swapu.
|
||||||
|
- Společná postranní navigace, modré akcenty, světlý a tmavý režim bez závislosti na Bootstrapu.
|
||||||
|
|
||||||
|
## Technologie a požadavky
|
||||||
|
|
||||||
|
| Část | Požadavky |
|
||||||
|
| --- | --- |
|
||||||
|
| Webový server | Apache, například v XAMPP; pro lokální vývoj lze použít i vestavěný PHP server. |
|
||||||
|
| PHP | Projekt je lokálně ověřený na PHP 8.2.12. Používané závislosti jsou uvedené v `Web/composer.lock`. |
|
||||||
|
| Databáze | MySQL/MariaDB; původní SQL dump pochází z MySQL 8.0.41. |
|
||||||
|
| PHP moduly | `mysqli` s `mysqlnd` pro `get_result()`, `session`, `fileinfo`; `zip` pro ZIP operace. Testy vykreslení používají také `dom`. |
|
||||||
|
| Závislosti | Composer a `phpseclib/phpseclib` řady 3; v repozitáři je také adresář `Web/vendor`. |
|
||||||
|
| Úložiště | Dostupný SFTP server a účet s přístupem k požadované cestě. |
|
||||||
|
| Statistiky | SSH přístup k Linux serveru, příkazy `df`, `free`, `vmstat`, `awk`, `tail` a čitelný `/proc/mdstat` pro Linux MD RAID. |
|
||||||
|
| Prohlížeč | Moderní prohlížeč s podporou nativního `<dialog>`. PDF náhled načítá PDF.js z CDN. |
|
||||||
|
|
||||||
|
Rozbalování archivů kromě ZIP může vyžadovat další nástroje, například UnRAR, 7-Zip nebo tar podle zvoleného formátu a operačního systému. PHP musí mít zapisovatelný dočasný adresář pro práci s archivy.
|
||||||
|
|
||||||
|
## Rychlé zprovoznění
|
||||||
|
|
||||||
|
### 1. Stažení projektu a závislostí
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/michalcz10/USB-RAID-Array.git
|
||||||
|
cd USB-RAID-Array/Web
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
Pokud už projekt máš stažený, spusť `composer install` v jeho adresáři `Web`. Pro instalaci podle existujícího lock souboru používej `install`.
|
||||||
|
|
||||||
|
### 2. Databáze a první správce
|
||||||
|
|
||||||
|
Postup je v [dokumentaci databáze](Documentation/Databaze.md). Obsahuje dvě varianty:
|
||||||
|
|
||||||
|
1. Import [původního SQL dumpu](Documentation/usbraidlogin.sql) a [doplnění tabulky pro sdílení](Documentation/share.sql).
|
||||||
|
2. Ruční vytvoření databáze a obou tabulek.
|
||||||
|
|
||||||
|
Součástí návodu je vytvoření prvního administrátora s bcrypt hashem hesla. Původní dump obsahuje také uživatelská data a sám nevytváří databázi ani tabulku `share`.
|
||||||
|
|
||||||
|
### 3. Konfigurace připojení
|
||||||
|
|
||||||
|
V souboru `Web/config/config.php` nastav vlastní připojení. Následující hodnoty jsou příklady; nahrazují odpovídající proměnné v existujícím souboru:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$DB_HOST = 'localhost';
|
||||||
|
$DB_USER = 'usbraid_app';
|
||||||
|
$DB_PASS = 'DOPLNTE_HESLO_DB';
|
||||||
|
$DB_NAME = 'usbraidlogin';
|
||||||
|
|
||||||
|
$SFTP_HOST = 'ADRESA_SFTP_SERVERU';
|
||||||
|
$SFTP_USER = 'SFTP_UZIVATEL';
|
||||||
|
$SFTP_PASS = 'DOPLNTE_HESLO_SFTP';
|
||||||
|
|
||||||
|
$SSH_HOST = 'ADRESA_SSH_SERVERU';
|
||||||
|
$SSH_USER = 'SSH_UZIVATEL';
|
||||||
|
$SSH_PASS = 'DOPLNTE_HESLO_SSH';
|
||||||
|
```
|
||||||
|
|
||||||
|
SFTP a SSH mohou používat stejný server, ale mají samostatné konfigurační proměnné. Aktuální pomocné funkce používají výchozí port 22 a přihlášení heslem. Přihlašovací údaje do webu se ověřují proti tabulce `users`; jednotliví weboví uživatelé sdílejí nakonfigurované SFTP připojení.
|
||||||
|
|
||||||
|
Výchozí vzdálená cesta se načítá z `users.defPath` po přihlášení. Nastav například `/mnt/raid` podle skutečného připojení úložiště na vzdáleném serveru. Nejde o cestu na počítači s XAMPP. SFTP účet musí mít také odpovídající oprávnění v Linuxu.
|
||||||
|
|
||||||
|
### 4. Spuštění přes XAMPP
|
||||||
|
|
||||||
|
1. Spusť Apache a MySQL v XAMPP Control Panelu.
|
||||||
|
2. Zpřístupni adresář `Web` jako `C:\xampp\htdocs\Web` — zkopírováním nebo již nastaveným symlinkem.
|
||||||
|
3. Otevři `http://localhost/Web/` a přihlas se vytvořeným účtem.
|
||||||
|
4. Ve správě uživatelů nastav další účty, jejich vzdálené cesty a oprávnění.
|
||||||
|
|
||||||
|
Při úpravách projektu používej soubory, které Apache skutečně obsluhuje. U kopie v `htdocs` je potřeba změny překopírovat; symlink odkazuje přímo na pracovní adresář.
|
||||||
|
|
||||||
|
Alternativně lze z kořene repozitáře spustit lokální PHP server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
php -S 127.0.0.1:8080 -t Web
|
||||||
|
```
|
||||||
|
|
||||||
|
Pak otevři `http://127.0.0.1:8080/`. Databáze a vzdálený SFTP/SSH server musí být dostupné i v tomto režimu.
|
||||||
|
|
||||||
|
## Struktura projektu
|
||||||
|
|
||||||
|
```text
|
||||||
|
USB-RAID-Array/
|
||||||
|
├── README.md
|
||||||
|
├── LICENSE
|
||||||
|
├── Documentation/
|
||||||
|
│ ├── Databaze.md # import, ruční vytvoření tabulek a první správce
|
||||||
|
│ ├── usbraidlogin.sql # původní dump tabulky users včetně dat
|
||||||
|
│ ├── share.sql # doplnění schématu pro sdílené odkazy
|
||||||
|
│ └── ... # původní technická dokumentace a podklady
|
||||||
|
├── tests/
|
||||||
|
│ ├── workspace_test.php
|
||||||
|
│ ├── serverstats_test.php
|
||||||
|
│ └── remotepaths_test.php
|
||||||
|
└── Web/
|
||||||
|
├── index.php # přihlášení
|
||||||
|
├── config/config.php # databáze, SFTP a SSH
|
||||||
|
├── includes/workspace.php
|
||||||
|
├── content/
|
||||||
|
│ ├── adminpanel.php
|
||||||
|
│ ├── changepassword.php
|
||||||
|
│ └── ftp/ # správce souborů, náhledy a statistiky
|
||||||
|
├── css/ # společné styly a rozložení
|
||||||
|
├── js/ # téma a dialogy
|
||||||
|
├── composer.json
|
||||||
|
└── vendor/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kontroly
|
||||||
|
|
||||||
|
Z kořene repozitáře:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
php tests/workspace_test.php
|
||||||
|
php tests/serverstats_test.php
|
||||||
|
php tests/remotepaths_test.php
|
||||||
|
node --check Web/js/theme.js
|
||||||
|
node --check Web/js/ui.js
|
||||||
|
node --check Web/content/ftp/js/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
PHP testy používají testovací data a nepotřebují připojení k databázi ani SSH. Kontrolují vykreslení stránek, navigaci podle role, zpracování statistik a stavy RAID polí. Node.js slouží u uvedených příkazů ke kontrole syntaxe; pro běh aplikace není potřeba.
|
||||||
|
|
||||||
|
## Časté problémy
|
||||||
|
|
||||||
|
| Problém | Co zkontrolovat |
|
||||||
|
| --- | --- |
|
||||||
|
| Chyba připojení k databázi | Běžící MySQL, název databáze, `$DB_*` a oprávnění databázového účtu. |
|
||||||
|
| Chybí tabulka `users` nebo `share` | Dokončení [nastavení databáze](Documentation/Databaze.md). |
|
||||||
|
| Přihlášení do webu nefunguje | `pswd` musí obsahovat bcrypt hash, nikoli samotné heslo. |
|
||||||
|
| `Login Failed` nebo `SSH login failed` | Nastavení SFTP/SSH, dostupnost serveru a vzdálená oprávnění. |
|
||||||
|
| Nefunguje upload větších souborů | `upload_max_filesize`, `post_max_size`, `max_file_uploads` a časové limity PHP/serveru. Po změně `php.ini` v XAMPP restartuj Apache. |
|
||||||
|
| Statistiky úložiště jsou stejné | `/` a uživatelská výchozí cesta mohou ležet na stejném souborovém systému. |
|
||||||
|
| RAID uvádí `Not configured` | Na připojeném serveru není v `/proc/mdstat` detekováno Linux MD pole; RAID na hostiteli virtuálního serveru se tímto způsobem nezobrazuje. |
|
||||||
|
| PDF se nenačte | Dostupnost PDF souboru, přístup přes SFTP a načtení PDF.js z CDN. |
|
||||||
|
| Zobrazuje se staré rozhraní | Obnov stránku včetně cache a ověř, že XAMPP načítá správnou kopii projektu. |
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Projekt je dostupný pod [licencí MIT](LICENSE). Autorem je Michal Sedlák.
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(git add *)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-13
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
// Autoload dependencies (phpseclib)
|
// Autoload dependencies (phpseclib)
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../includes/remotepaths.php';
|
||||||
|
|
||||||
use phpseclib3\Net\SFTP;
|
use phpseclib3\Net\SFTP;
|
||||||
use phpseclib3\Net\SSH2;
|
use phpseclib3\Net\SSH2;
|
||||||
@@ -56,17 +57,4 @@ function getSSHConnection(): SSH2 {
|
|||||||
return $ssh;
|
return $ssh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Utility: Normalise a path */
|
|
||||||
function normalizePath(string $path): string {
|
|
||||||
$parts = array_filter(explode('/', $path), fn($part) => $part !== '' && $part !== '.');
|
|
||||||
$stack = [];
|
|
||||||
foreach ($parts as $part) {
|
|
||||||
if ($part === '..') {
|
|
||||||
array_pop($stack);
|
|
||||||
} else {
|
|
||||||
$stack[] = $part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '/' . implode('/', $stack);
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
+36
-54
@@ -10,6 +10,7 @@ ini_set('display_errors', 1);
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
require_once __DIR__ . '/../config/config.php';
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
require_once __DIR__ . '/../includes/workspace.php';
|
||||||
// $username = "UNAME";
|
// $username = "UNAME";
|
||||||
// $password = "PSWD";
|
// $password = "PSWD";
|
||||||
// $db = "usbraidlogin";
|
// $db = "usbraidlogin";
|
||||||
@@ -136,15 +137,15 @@ function CheckPassword($password) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Admin Panel</title>
|
<title>Admin Panel</title>
|
||||||
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../', 'content/css/adminpanel.css') ?>">
|
||||||
<link rel="stylesheet" href="css/adminpanel.css">
|
|
||||||
<script src="../js/bootstrap.bundle.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(uname) {
|
function confirmDelete(uname) {
|
||||||
if (confirm('Are you sure you want to delete this user?')) {
|
if (confirm('Are you sure you want to delete this user?')) {
|
||||||
@@ -152,45 +153,39 @@ function CheckPassword($password) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="<?= workspaceAsset('../', 'js/theme.js') ?>"></script>
|
||||||
|
<script src="<?= workspaceAsset('../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-center">
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'users',
|
||||||
<i class="bi"></i>
|
'section' => 'User management',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Workspace administration',
|
||||||
</button>
|
'title' => 'User management',
|
||||||
</div>
|
'description' => 'Manage accounts, storage locations and access permissions.',
|
||||||
|
'class' => 'users-page',
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<section class="users-layout">
|
||||||
|
|
||||||
<div class="custom-container dark-box">
|
|
||||||
<header class="row border-bottom m-5">
|
|
||||||
<h1>USB RAID Array</h1>
|
|
||||||
<div class="mb-3 p-3">
|
|
||||||
<a href="logout.php" class="btn btn-danger">Logout</a>
|
|
||||||
<a href="changepassword.php" class="btn btn-warning">Change Password</a>
|
|
||||||
<a href="ftp/index.php" class="btn btn-primary">SFTP</a>
|
|
||||||
<a href="ftp/serverstat.php" class="btn btn-primary">Server Status</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<section class="row">
|
|
||||||
<article class="col border border-2 border-primary rounded p-2 surface-panel">
|
|
||||||
<h2 class="mb-4">User Management</h2>
|
|
||||||
<?php if ($message): ?>
|
<?php if ($message): ?>
|
||||||
<div class="alert alert-<?php echo $message_type === 'success' ? 'success' : 'danger'; ?>">
|
<div class="alert alert-<?php echo $message_type === 'success' ? 'success' : 'danger'; ?>">
|
||||||
<?php echo htmlspecialchars($message); ?>
|
<?php echo htmlspecialchars($message); ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<form method="POST" action="" class="mb-4 add-user-form">
|
<div class="content-panel user-create"><div class="panel-heading"><h2>Add a member</h2><p>Set up an account and choose its access.</p></div>
|
||||||
|
<form method="POST" action="" class="add-user-form">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Username</label>
|
<label class="form-label" for="memberUsername">Username</label>
|
||||||
<input type="text" name="uname" class="form-control" placeholder="Username" required>
|
<input type="text" name="uname" id="memberUsername" class="form-control" placeholder="Username" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label" for="memberPassword">Password</label>
|
||||||
<input type="password" name="pswd" class="form-control" placeholder="Password" required>
|
<input type="password" name="pswd" id="memberPassword" class="form-control" placeholder="Password" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Default Path</label>
|
<label class="form-label" for="memberPath">Default path</label>
|
||||||
<input type="text" name="defPath" class="form-control" placeholder="Default Path (e.g., /mnt/raid)" required>
|
<input type="text" name="defPath" id="memberPath" class="form-control" placeholder="Default Path (e.g., /mnt/raid)" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column align-items-center mb-3">
|
<div class="d-flex flex-column align-items-center mb-3">
|
||||||
<div class="form-check mb-2">
|
<div class="form-check mb-2">
|
||||||
@@ -211,9 +206,11 @@ function CheckPassword($password) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Add User</button>
|
<button type="submit" class="btn btn-primary">Add User</button>
|
||||||
</form>
|
</form></div>
|
||||||
<h3 class="mb-3">Users List</h3>
|
<div class="content-panel user-list"><div class="panel-heading"><h2>Workspace members</h2><p>Accounts and permissions for your storage.</p></div>
|
||||||
<table class="table table-bordered">
|
|
||||||
|
|
||||||
|
<div class="table-scroll"><table class="table users-table">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
@@ -240,25 +237,10 @@ function CheckPassword($password) {
|
|||||||
</tr>
|
</tr>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table></div>
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
|
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</section>
|
||||||
|
<?php renderWorkspaceEnd(); ?>
|
||||||
<script src="../js/theme.js"></script>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ if (!isset($_SESSION['uname'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/../config/config.php';
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
require_once __DIR__ . '/../includes/workspace.php';
|
||||||
// $username = "UNAME";
|
// $username = "UNAME";
|
||||||
// $password = "PSWD";
|
// $password = "PSWD";
|
||||||
// $db = "usbraidlogin";
|
// $db = "usbraidlogin";
|
||||||
@@ -93,82 +94,58 @@ function CheckPassword($password) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
<title>Change Password</title>
|
<title>Change Password</title>
|
||||||
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../', 'content/css/changepassword.css') ?>">
|
||||||
<link rel="stylesheet" href="css/changepassword.css">
|
<script src="<?= workspaceAsset('../', 'js/theme.js') ?>"></script>
|
||||||
<script src="../js/bootstrap.bundle.js"></script>
|
<script src="<?= workspaceAsset('../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-center">
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'account',
|
||||||
<i class="bi"></i>
|
'section' => 'Account settings',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Your account',
|
||||||
</button>
|
'title' => 'Account settings',
|
||||||
</div>
|
'description' => 'Keep your workspace account secure and up to date.',
|
||||||
|
'class' => 'account-page',
|
||||||
<div class="custom-container dark-box">
|
]); ?>
|
||||||
<header class="row border-bottom my-3 my-md-5">
|
|
||||||
<h1>USB RAID Array</h1>
|
|
||||||
<div class="mb-3 p-3">
|
<section class="account-layout">
|
||||||
<div class="btn-group-responsive">
|
<article class="content-panel">
|
||||||
<a href="logout.php" class="btn btn-danger">Logout</a>
|
<div class="panel-heading"><h2>Change password</h2><p>Choose a strong password to protect your files.</p></div>
|
||||||
<a href="ftp/index.php" class="btn btn-primary">Back to Files</a>
|
|
||||||
<?php if (isset($_SESSION["admin"]) && $_SESSION["admin"] == true) { ?>
|
|
||||||
<a href="adminpanel.php" class="btn btn-primary">Admin Panel</a>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="row content-section">
|
|
||||||
<article class="col-12 border border-2 border-primary rounded p-2 p-md-4 surface-panel">
|
|
||||||
<h2 class="mb-4">Change Password</h2>
|
|
||||||
|
|
||||||
<?php if ($message): ?>
|
<?php if ($message): ?>
|
||||||
<div class="alert alert-<?php echo $messageType; ?> mb-4">
|
<div class="alert alert-<?php echo $messageType; ?> mb-4">
|
||||||
<?php echo $message; ?>
|
<?php echo $message; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="POST" action="" class="mb-4 change-password-form">
|
<form method="POST" action="" class="mb-4 change-password-form">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Current Password</label>
|
<label class="form-label" for="currentPassword">Current password</label>
|
||||||
<input type="password" name="current_password" class="form-control" required>
|
<input type="password" id="currentPassword" autocomplete="current-password" name="current_password" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">New Password</label>
|
<label class="form-label" for="newPassword">New password</label>
|
||||||
<input type="password" name="new_password" class="form-control" required>
|
<input type="password" id="newPassword" autocomplete="new-password" name="new_password" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Confirm New Password</label>
|
<label class="form-label" for="confirmPassword">Confirm new password</label>
|
||||||
<input type="password" name="confirm_password" class="form-control" required>
|
<input type="password" id="confirmPassword" autocomplete="new-password" name="confirm_password" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Change Password</button>
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
<aside class="account-help"><span class="eyebrow">Password requirements</span><h3>A little extra protection</h3><p>Use at least 8 characters, one uppercase letter and one number.</p><p>You can return to your files at any time using the workspace navigation.</p></aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
|
<?php renderWorkspaceEnd(); ?>
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3 flex-wrap justify-content-center">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="../js/theme.js"></script>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,66 +1,13 @@
|
|||||||
html, body {
|
.users-layout { display:grid; grid-template-columns:300px minmax(0,1fr); align-items:start; gap:22px; }
|
||||||
height: 100%;
|
.users-layout > .alert { grid-column:1/-1; margin:0; }
|
||||||
margin: 0;
|
.add-user-form { margin:0; text-align:left; }
|
||||||
}
|
.add-user-form .form-check { width:100%; margin-bottom:12px; font-size:12px; }
|
||||||
|
.add-user-form .btn { width:100%; }
|
||||||
.custom-container {
|
.users-table { min-width:650px; }
|
||||||
display: flex;
|
.users-table th { font-size:10px; padding:12px 10px; white-space:nowrap; }
|
||||||
flex-direction: column;
|
.users-table td { font-size:12px; padding:14px 10px; }
|
||||||
min-height: 100vh;
|
.users-table td:first-child { font-weight:600; }
|
||||||
max-width: 60%;
|
.users-table td:nth-child(3) { font-family:ui-monospace,monospace; white-space:nowrap; }
|
||||||
margin: 0 auto;
|
.user-create .form-label { font-size:12px; }
|
||||||
}
|
@media(max-width:1200px) { .users-layout { grid-template-columns:1fr; } .add-user-form { display:grid; grid-template-columns:1fr 1fr; gap:0 24px; } .add-user-form > .d-flex { grid-column:1/-1; display:grid; grid-template-columns:1fr 1fr; gap:8px; } .add-user-form > .btn { width:auto; justify-self:start; } }
|
||||||
.custom-container.dark-box {
|
@media(max-width:600px) { .add-user-form { display:block; } .add-user-form > .d-flex { display:flex; align-items:flex-start; gap:0; } }
|
||||||
min-height: calc(100vh - 4rem);
|
|
||||||
width: min(60%, 1200px);
|
|
||||||
}
|
|
||||||
section.row {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
@media (max-width: 1120px) {
|
|
||||||
.custom-container {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
.custom-container.dark-box {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.custom-container.dark-box {
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
min-height: calc(100vh - 2rem);
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.add-user-form {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.form-check {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.form-check-input {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.form-check-label {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,56 +1,6 @@
|
|||||||
.container-fluid {
|
.account-layout { display:grid; grid-template-columns:minmax(0,560px) minmax(200px,1fr); align-items:start; gap:40px; }
|
||||||
width: 100%;
|
.change-password-form { margin:0; }
|
||||||
padding-right: 15px;
|
.change-password-form .form-label { font-size:12px; }
|
||||||
padding-left: 15px;
|
.account-help { max-width:320px; padding:24px 0; color:var(--muted); font-size:13px; }
|
||||||
margin-right: auto;
|
.account-help h3 { color:var(--text); margin-top:12px; font-size:18px; }
|
||||||
margin-left: auto;
|
@media(max-width:1000px) { .account-layout { grid-template-columns:1fr; gap:16px; } .account-help { max-width:100%; padding:12px 4px; } }
|
||||||
}
|
|
||||||
|
|
||||||
.content-section {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-password-form {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.change-password-form {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group-responsive {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+15
-141
@@ -1,142 +1,16 @@
|
|||||||
html, body {
|
.pathbar { display:flex; align-items:center; flex-wrap:wrap; gap:12px; padding:13px 16px; background:var(--surface); border:1px solid var(--line); border-radius:10px; margin-bottom:20px; font-size:12px; overflow-wrap:anywhere; }
|
||||||
height: 100%;
|
.pathbar a { color:var(--text); } .pathbar a:first-child { color:var(--blue); } .path-separator { color:var(--muted); } .path-label { margin-left:auto; color:var(--muted); font:11px ui-monospace,monospace; }
|
||||||
margin: 0;
|
.file-panel { border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); overflow:hidden; }
|
||||||
overflow-x: hidden;
|
.browser-toolbar { padding:20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--line); }
|
||||||
overflow-y: auto;
|
.search-field { display:flex; align-items:center; gap:10px; margin:0; padding:0 12px; border:1px solid var(--line); border-radius:8px; width:360px; max-width:100%; background:var(--bg); color:var(--muted); font-weight:400; }
|
||||||
}
|
.search-field > span { font-size:24px; } .search-field input { width:100%; min-width:0; padding:10px 0; border:0; background:transparent; color:var(--text); outline:none; } .search-field:focus-within { outline:2px solid var(--blue); } .search-field input::placeholder { color:var(--muted); } kbd { font:11px ui-monospace,monospace; padding:0 5px; border:1px solid var(--line); border-radius:3px; }
|
||||||
|
.browser-toolbar select { width:130px; margin-left:auto; font-size:12px; padding:10px; }
|
||||||
|
.action-buttons { display:flex; flex-wrap:wrap; gap:8px; padding:18px 20px 0; } .action-buttons .btn { font-size:12px; } .action-buttons .btn-primary { order:-1; }
|
||||||
|
.dropzone { margin:18px 20px 22px; border:1px dashed var(--line); background:var(--bg); border-radius:9px; display:flex; align-items:center; justify-content:center; gap:14px; padding:20px; text-align:left; }
|
||||||
|
.dropzone.dragover { border-color:var(--blue); background:var(--blue-soft); } .upload-symbol { display:grid; place-items:center; width:36px; height:36px; border:1px solid var(--line); border-radius:9px; background:var(--surface); font-size:22px; color:var(--blue); } .dropzone strong { font-weight:550; font-size:12px; } .dropzone small { display:block; color:var(--muted); font-size:11px; margin-top:3px; }
|
||||||
|
.file-table-scroll { overflow-x:auto; } .file-table { min-width:650px; } .file-table th { padding:12px 20px; } .file-table td { padding:14px 20px; font-size:12px; } .file-table th:first-child { width:48%; } .file-table th:nth-child(2) { width:13%; } .file-table th:last-child { width:39%; } .file-table td:first-child { font-weight:550; } .file-table td:first-child a { color:var(--text); } .file-table .btn { font-size:11px; padding:5px 8px; min-height:30px; background:transparent; border-color:transparent; } .file-table .btn:hover { background:var(--surface); border-color:var(--line); } .file-table td:last-child { white-space:nowrap; } .file-table td:nth-child(2) { color:var(--muted); font-family:ui-monospace,monospace; font-size:11px; }
|
||||||
|
.file-icon { display:inline-grid; place-items:center; width:30px; height:32px; margin-right:10px; color:var(--muted); background:var(--surface-alt); border-radius:7px; vertical-align:middle; font-size:18px; } .folder-icon { color:var(--blue); background:var(--blue-soft); } .file-table .badge { margin-left:5px; }
|
||||||
|
.browser-status { display:flex; justify-content:space-between; padding:13px 20px; color:var(--muted); font-size:11px; } .empty-state { text-align:center; padding:48px 20px; color:var(--muted); } .empty-state > span { font-size:38px; } .empty-state h3 { color:var(--text); margin:6px 0; } .empty-state p { margin:0; }
|
||||||
|
.file-content > .alert { margin:16px 20px; }
|
||||||
|
|
||||||
.custom-container {
|
@media(max-width:760px) { .path-label { display:none; } .browser-toolbar { padding:14px; gap:8px; } .search-field { width:auto; flex:1; min-width:0; } .browser-toolbar select { width:100px; } .action-buttons { padding:14px 14px 0; } .dropzone { margin:14px; } }
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.row {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
article.col-8 {
|
|
||||||
max-width: 1600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1800px) {
|
|
||||||
article.col-8 {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.custom-container {
|
|
||||||
height: auto;
|
|
||||||
min-height: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.row {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: auto;
|
|
||||||
min-height: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
article {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 20px 0;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
text-align: left;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone {
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
border: 2px dashed #007bff;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone.dragover {
|
|
||||||
background-color: #e0f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Give the SFTP file browser its own surface, distinct from the outer page. */
|
|
||||||
article.col-8 {
|
|
||||||
background: linear-gradient(145deg, var(--sftp-bg-start), var(--sftp-bg-end));
|
|
||||||
border-color: rgba(77, 145, 255, 0.3) !important;
|
|
||||||
border-radius: 1rem !important;
|
|
||||||
box-shadow: 0 12px 32px rgba(3, 8, 18, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-bs-theme="light"] article.col-8 {
|
|
||||||
background: linear-gradient(145deg, #ffffff, #edf3fb);
|
|
||||||
border-color: rgba(59, 130, 246, 0.4) !important;
|
|
||||||
box-shadow: 0 18px 48px rgba(31, 58, 92, 0.1);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,54 +1,6 @@
|
|||||||
.editor-container {
|
.editor-container { position:relative; border:1px solid var(--line); border-radius:10px; overflow:hidden; margin-bottom:20px; }
|
||||||
border: 1px solid #ccc;
|
#editor { width:100%; min-height:55vh; padding:16px; font:13px/1.65 ui-monospace,Consolas,monospace; white-space:pre; overflow:auto; resize:vertical; tab-size:4; border:0; border-radius:0; background:var(--bg); }
|
||||||
border-radius: 4px;
|
.editor-container:has(.line-numbers) #editor { padding-left:64px; }
|
||||||
margin-bottom: 20px;
|
.line-numbers { position:absolute; left:0; top:0; width:48px; text-align:right; padding:16px 10px 16px 0; border-right:1px solid var(--line); color:var(--muted); user-select:none; font:13px/1.65 ui-monospace,Consolas,monospace; overflow:hidden; z-index:1; background:var(--surface-alt); }
|
||||||
position: relative;
|
.readonly-notice { color:var(--muted); font-size:12px; }
|
||||||
}
|
.editor-page .d-flex { flex-wrap:wrap; gap:8px; }
|
||||||
|
|
||||||
#editor {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 400px;
|
|
||||||
font-family: monospace;
|
|
||||||
padding: 10px;
|
|
||||||
white-space: pre;
|
|
||||||
overflow: auto;
|
|
||||||
resize: vertical;
|
|
||||||
tab-size: 4;
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
padding-left: 55px; /* Make room for line numbers */
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-numbers {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 45px;
|
|
||||||
text-align: right;
|
|
||||||
padding: 10px 5px 10px 0;
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
color: #999;
|
|
||||||
user-select: none;
|
|
||||||
font-family: monospace;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 1;
|
|
||||||
background-color: #f8f9fa; /* Light mode default */
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-bs-theme="dark"] .line-numbers {
|
|
||||||
background-color: #212529; /* Dark mode background */
|
|
||||||
border-right-color: #495057; /* Darker border for dark mode */
|
|
||||||
color: #6c757d; /* Lighter text for dark mode */
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.readonly-notice {
|
|
||||||
color: #dc3545;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|||||||
+15
-253
@@ -1,253 +1,15 @@
|
|||||||
body {
|
.pdf-workspace { padding:16px; }
|
||||||
min-height: 100vh;
|
#pdfContainer { position:relative; width:100%; min-height:380px; display:flex; flex-direction:column; align-items:center; padding:16px 0; overflow:auto; background:var(--bg); border:1px solid var(--line); border-radius:8px; }
|
||||||
margin: 0;
|
#loadingMessage { position:absolute; inset:0; background:var(--bg); z-index:1; color:var(--muted); }
|
||||||
padding: 0;
|
.page-container { background:white; box-shadow:0 4px 20px #0002; max-width:100%; overflow:hidden; }
|
||||||
}
|
.page-container canvas { display:block; }
|
||||||
#pdfContainer {
|
.controls { display:flex; justify-content:center; margin-top:16px; }
|
||||||
width: 100%;
|
.controls .btn-group { flex-wrap:wrap; justify-content:center; gap:6px; }
|
||||||
min-height: calc(100vh - 80px);
|
.controls .btn { font-size:12px; }
|
||||||
display: flex;
|
.actionButton { display:flex; justify-content:center; margin-top:12px; }
|
||||||
flex-direction: column;
|
.actionButton .btn { margin:0; }
|
||||||
align-items: center;
|
.pdf-workspace.fullscreen-mode { position:fixed; inset:0; width:100%; height:100dvh; z-index:9999; background:var(--bg); padding:0; border:0; border-radius:0; display:flex; flex-direction:column; }
|
||||||
padding: 20px 0;
|
.fullscreen-mode #pdfContainer { flex:1; height:100%; min-height:0; padding:0; border:0; border-radius:0; justify-content:center; }
|
||||||
}
|
.fullscreen-mode .controls { position:absolute; bottom:12px; left:50%; transform:translateX(-50%); margin:0; padding:8px; border:1px solid var(--line); border-radius:10px; background:var(--surface); max-width:calc(100% - 24px); width:max-content; z-index:2; }
|
||||||
#loadingMessage {
|
.fullscreen-mode .actionButton { display:none; }
|
||||||
position: absolute;
|
@media(max-width:600px) { .pdf-workspace { padding:10px; } .controls .btn { padding:7px 10px; } }
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
background-color: var(--bs-body-bg);
|
|
||||||
}
|
|
||||||
[data-bs-theme="dark"] #loadingMessage {
|
|
||||||
background-color: rgba(33, 37, 41, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-bs-theme="light"] #loadingMessage {
|
|
||||||
background-color: rgba(248, 249, 250, 0.9);
|
|
||||||
}
|
|
||||||
.page-container {
|
|
||||||
background-color: white;
|
|
||||||
margin: 10px;
|
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.file-info table {
|
|
||||||
table-layout: fixed;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.actionButton {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--bs-body-bg);
|
|
||||||
z-index: 9999;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode header,
|
|
||||||
.fullscreen-mode footer,
|
|
||||||
.fullscreen-mode .theme-toggle,
|
|
||||||
.fullscreen-mode .actionButton {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode #pdfContainer {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .controls {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
margin: 0;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: rgba(var(--bs-body-bg-rgb), 0.3);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
opacity: 0.3;
|
|
||||||
z-index: 1000;
|
|
||||||
border-radius: .375rem;
|
|
||||||
width: auto;
|
|
||||||
max-width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .controls:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background-color: rgba(var(--bs-body-bg-rgb), 0.9);
|
|
||||||
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode[data-bs-theme="light"] .btn-light {
|
|
||||||
background-color: rgba(233, 236, 239, 0.7);
|
|
||||||
border-color: rgba(222, 226, 230, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode[data-bs-theme="dark"] .btn-light {
|
|
||||||
background-color: rgba(52, 58, 64, 0.7);
|
|
||||||
border-color: rgba(73, 80, 87, 0.7);
|
|
||||||
}
|
|
||||||
.fullscreen-mode .btn-light:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .btn-light.disabled {
|
|
||||||
background-color: rgba(var(--bs-body-bg-rgb), 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .page-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode canvas {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100vh;
|
|
||||||
width: 100% !important;
|
|
||||||
height: auto !important;
|
|
||||||
margin: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
.fullscreen-mode .btn-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .btn-group .btn {
|
|
||||||
min-width: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .btn-group .btn.disabled {
|
|
||||||
min-width: 120px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
}
|
|
||||||
/* Mobile-specific adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.fullscreen-mode .controls {
|
|
||||||
bottom: 5px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
width: auto;
|
|
||||||
max-width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .btn-group .btn {
|
|
||||||
min-width: 36px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .btn-group .btn.disabled {
|
|
||||||
min-width: 110px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode #pageNum,
|
|
||||||
.fullscreen-mode #pageCount {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-mode .btn-group {
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add spacing between page numbers and "of" text */
|
|
||||||
.fullscreen-mode .btn-light.disabled span {
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme-specific button styles */
|
|
||||||
[data-bs-theme="light"] .btn-light {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
border-color: #dee2e6;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-bs-theme="light"] .btn-light:hover {
|
|
||||||
background-color: #dde2e6;
|
|
||||||
border-color: #ced4da;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-bs-theme="dark"] .btn-light {
|
|
||||||
background-color: #343a40;
|
|
||||||
border-color: #495057;
|
|
||||||
color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-bs-theme="dark"] .btn-light:hover {
|
|
||||||
background-color: #495057;
|
|
||||||
border-color: #6c757d;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button group specific styles */
|
|
||||||
.btn-group .btn-light {
|
|
||||||
margin: 0 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group .btn-light.disabled {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,124 +1,18 @@
|
|||||||
/* Layout */
|
.stats-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:22px; }
|
||||||
body {
|
.stats-cell { min-width:0; }
|
||||||
min-height: 100vh;
|
.monitor-card { padding:0; overflow:hidden; box-shadow:var(--shadow); }
|
||||||
margin: 0;
|
.monitor-card .card-header { padding:18px 22px; background:var(--surface); border-bottom:1px solid var(--line); }
|
||||||
padding: 0;
|
.monitor-card h3 { font-size:14px; }
|
||||||
display: flex;
|
.monitor-card .card-body { padding:24px; min-height:235px; }
|
||||||
flex-direction: column;
|
.monitor-card .card-body > .text-muted { overflow-wrap:anywhere; }
|
||||||
}
|
.monitor-card .row { flex-wrap:nowrap; gap:10px; }
|
||||||
|
.monitor-card .col-4 { min-width:0; }
|
||||||
footer {
|
.monitor-card .fw-bold { font-size:13px; overflow-wrap:anywhere; margin-top:6px; }
|
||||||
margin-top: auto;
|
.monitor-card .small { font-size:11px; }
|
||||||
padding: 20px;
|
.cpu-gauge { min-height:185px; display:grid; place-items:center; }
|
||||||
border-top: 1px solid var(--bs-border-color);
|
.raid-summary { text-align:center; padding:20px 0; }
|
||||||
width: 100%;
|
.raid-summary > .ui-icon { font-size:30px; margin-bottom:12px; }
|
||||||
}
|
.monitor-card .progress { margin-top:8px; }
|
||||||
|
.monitor-card .resource-section + .resource-section { margin-top:24px; }
|
||||||
/* Monitor Cards */
|
@media(max-width:1000px) { .stats-grid { grid-template-columns:1fr; } }
|
||||||
.monitor-card {
|
@media(max-width:600px) { .monitor-card .card-body { padding:18px; } .monitor-card .row { gap:6px; } .monitor-card .p-2 { padding:10px 6px; } }
|
||||||
transition: transform 0.2s ease-in-out;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitor-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitor-card .card-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
min-height: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitor-header {
|
|
||||||
background-color: var(--bs-body-bg);
|
|
||||||
border-bottom: 1px solid var(--bs-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
background-color: var(--bs-tertiary-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CPU Gauge */
|
|
||||||
.cpu-gauge {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cpu-gauge canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cpu-gauge .position-relative {
|
|
||||||
position: absolute !important;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -55%);
|
|
||||||
z-index: 2;
|
|
||||||
width: auto;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cpu-gauge .h3 {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
line-height: 1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cpu-gauge .small {
|
|
||||||
opacity: 0.75;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme & Logo */
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid Spacing */
|
|
||||||
.row.g-4 {
|
|
||||||
--bs-gutter-y: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitor-card .card-body {
|
|
||||||
padding: 1rem;
|
|
||||||
min-height: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row.g-4 > [class*="col-"] {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,86 +1,11 @@
|
|||||||
html, body {
|
.media-container { display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:12px; padding:24px; background:var(--bg); border:1px solid var(--line); border-radius:10px; min-height:200px; }
|
||||||
height: 100%;
|
.media-container img,.media-container video { display:block; max-width:100%; max-height:65vh; object-fit:contain; flex-basis:100%; }
|
||||||
margin: 0;
|
.media-container audio { display:block; width:100%; max-width:680px; margin:28px auto; flex-basis:100%; }
|
||||||
overflow-x: hidden;
|
.media-container .btn { margin:0; }
|
||||||
}
|
.file-info { max-width:760px; }
|
||||||
|
.file-info h4 { font-size:16px; }
|
||||||
.custom-container {
|
.file-info table { width:100%; table-layout:fixed; }
|
||||||
display: flex;
|
.file-info th { width:160px; }
|
||||||
flex-direction: column;
|
.file-info td { overflow-wrap:anywhere; font-size:12px; }
|
||||||
min-height: 100vh;
|
.media-page .input-group { flex-wrap:wrap; } .media-page .input-group input { flex:1; min-width:160px; }
|
||||||
margin: 0;
|
@media(max-width:600px) { .media-container { padding:12px; } .file-info th { width:110px; } }
|
||||||
padding: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 80vh;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: visible;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-container img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-container video {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-container audio {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
margin: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.file-info table {
|
|
||||||
table-layout: fixed;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.file-info table {
|
|
||||||
width: 95% !important;
|
|
||||||
}
|
|
||||||
.file-info th {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
.file-info td {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ $sftp = initializeSFTP();
|
|||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
|
||||||
$itemToDelete = $_POST['delete'];
|
$itemToDelete = $_POST['delete'];
|
||||||
$parentDir = dirname($itemToDelete);
|
$parentDir = remoteDirname($itemToDelete);
|
||||||
|
|
||||||
if ($parentDir === '/' || $parentDir === '.') {
|
if ($parentDir === '/' || $parentDir === '.') {
|
||||||
$parentDir = $defaultPath;
|
$parentDir = $defaultPath;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ try {
|
|||||||
$fullRemotePath = rtrim($currentPath, '/') . '/' . $file;
|
$fullRemotePath = rtrim($currentPath, '/') . '/' . $file;
|
||||||
|
|
||||||
$baseDirName = basename($remoteBasePath);
|
$baseDirName = basename($remoteBasePath);
|
||||||
$relPathFromBase = substr($fullRemotePath, strlen(dirname($remoteBasePath)) + 1);
|
$relPathFromBase = substr($fullRemotePath, strlen(rtrim(remoteDirname($remoteBasePath), '/')) + 1);
|
||||||
file_put_contents($logFile, "Processing: $fullRemotePath (relative: $relPathFromBase)\n", FILE_APPEND);
|
file_put_contents($logFile, "Processing: $fullRemotePath (relative: $relPathFromBase)\n", FILE_APPEND);
|
||||||
|
|
||||||
$isDir = $sftp->is_dir($fullRemotePath);
|
$isDir = $sftp->is_dir($fullRemotePath);
|
||||||
@@ -214,4 +214,4 @@ try {
|
|||||||
header("HTTP/1.1 500 Internal Server Error");
|
header("HTTP/1.1 500 Internal Server Error");
|
||||||
echo $errorMessage;
|
echo $errorMessage;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ $sftp = initializeSFTP();
|
|||||||
// Process extraction request
|
// Process extraction request
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) {
|
||||||
$archivePath = $_POST['archive'];
|
$archivePath = $_POST['archive'];
|
||||||
$dirPath = dirname($archivePath);
|
$dirPath = remoteDirname($archivePath);
|
||||||
$archiveName = basename($archivePath);
|
$archiveName = basename($archivePath);
|
||||||
$extension = pathinfo($archiveName, PATHINFO_EXTENSION);
|
$extension = pathinfo($archiveName, PATHINFO_EXTENSION);
|
||||||
|
|
||||||
|
|||||||
+78
-106
@@ -13,19 +13,21 @@
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../includes/workspace.php';
|
||||||
$sftp = initializeSFTP();
|
$sftp = initializeSFTP();
|
||||||
|
|
||||||
|
$defaultPath = normalizePath($defaultPath);
|
||||||
$currentPath = isset($_GET['path']) ? normalizePath($_GET['path']) : $defaultPath;
|
$currentPath = isset($_GET['path']) ? normalizePath($_GET['path']) : $defaultPath;
|
||||||
if (strpos($currentPath, $defaultPath) !== 0) {
|
if ($currentPath !== $defaultPath && strpos($currentPath, rtrim($defaultPath, '/') . '/') !== 0) {
|
||||||
$currentPath = $defaultPath;
|
$currentPath = $defaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$sftp->chdir($currentPath)) {
|
if (!$sftp->chdir($currentPath)) {
|
||||||
die("Failed to navigate to the selected folder: $currentPath");
|
die("Failed to navigate to the selected folder: $currentPath");
|
||||||
}
|
}
|
||||||
|
|
||||||
$items = $sftp->nlist();
|
$items = $sftp->nlist();
|
||||||
|
|
||||||
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
|
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
|
||||||
$videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
|
$videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
|
||||||
$audioExtensions = ['mp3', 'wav', 'm4a', 'flac', 'aac'];
|
$audioExtensions = ['mp3', 'wav', 'm4a', 'flac', 'aac'];
|
||||||
@@ -35,51 +37,41 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>FTP</title>
|
<title>Files · USB RAID</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="../../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/index.css') ?>">
|
||||||
<link rel="stylesheet" href="css/index.css">
|
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
|
||||||
<script src="../../js/bootstrap.bundle.js"></script>
|
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-center">
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'files',
|
||||||
<i class="bi"></i>
|
'section' => 'Files',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Your remote storage',
|
||||||
</button>
|
'title' => 'All files',
|
||||||
</div>
|
'description' => 'Everything in its place. Browse, manage and transfer your files.',
|
||||||
<div class="custom-container dark-box">
|
'connected' => true,
|
||||||
<header class="row border-bottom m-5">
|
]); ?>
|
||||||
<h1>USB RAID Array</h1>
|
<nav class="pathbar" aria-label="Directory path"><a href="index.php" title="Home directory">⌂ <span>Home</span></a><?php
|
||||||
<div class="mb-3 p-3">
|
$relativePath = trim(substr($currentPath, strlen(rtrim($defaultPath, '/'))), '/');
|
||||||
<a href="../logout.php" class="btn btn-danger">Logout</a>
|
$crumbPath = rtrim($defaultPath, '/');
|
||||||
<a href="../changepassword.php" class="btn btn-warning">Change Password</a>
|
foreach (array_filter(explode('/', $relativePath), 'strlen') as $segment):
|
||||||
<?php if (isset($_SESSION["admin"]) && $_SESSION["admin"] == true) { ?>
|
$crumbPath .= '/' . $segment;
|
||||||
<a href="../adminpanel.php" class="btn btn-primary">Admin Panel</a>
|
?><span class="path-separator" aria-hidden="true">/</span><a href="?path=<?= urlencode($crumbPath) ?>"><?= htmlspecialchars($segment) ?></a><?php endforeach; ?><span class="path-label" title="<?= htmlspecialchars($currentPath) ?>"><?= htmlspecialchars($currentPath) ?></span></nav>
|
||||||
<a href="serverstat.php" class="btn btn-primary">Server Status</a>
|
<section class="file-panel" aria-label="File browser">
|
||||||
<?php } ?>
|
<div class="browser-toolbar"><label class="search-field"><span aria-hidden="true">⌕</span><input id="fileSearch" type="search" placeholder="Search this folder…" aria-label="Search this folder"><kbd>/</kbd></label><select id="typeFilter" class="form-select" aria-label="Filter by type"><option value="all">All types</option><option value="folder">Folders</option><option value="file">Files</option></select></div>
|
||||||
</div>
|
<div class="file-content">
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="row">
|
|
||||||
|
|
||||||
<article class="col-8 border border-2 border-primary rounded p-2">
|
|
||||||
<div class="col">
|
|
||||||
<h4>Current Path: <?= htmlspecialchars($currentPath) ?></h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) { ?>
|
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) { ?>
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createFileModal">Create File</button>
|
<button type="button" class="btn" data-dialog="createFileModal">+ New file</button>
|
||||||
<button type="button" class="btn btn-success" onclick="document.getElementById('fileInput').click()">Upload Files</button>
|
<button type="button" class="btn btn-primary" onclick="document.getElementById('fileInput').click()">↑ Upload files</button>
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createDirModal">Create Directory</button>
|
<button type="button" class="btn" data-dialog="createDirModal">+ New folder</button>
|
||||||
<button type="button" class="btn btn-info" onclick="document.getElementById('dirInput').click()">Upload Directory</button>
|
<button type="button" class="btn btn-info" onclick="document.getElementById('dirInput').click()">Upload folder</button>
|
||||||
<input type="file" id="fileInput" multiple style="display: none;" onchange="handleFileSelect(event)">
|
<input type="file" id="fileInput" multiple style="display: none;" onchange="handleFileSelect(event)">
|
||||||
<input type="file" id="dirInput" webkitdirectory directory multiple style="display: none;" onchange="handleFileSelect(event)">
|
<input type="file" id="dirInput" webkitdirectory directory multiple style="display: none;" onchange="handleFileSelect(event)">
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
@@ -87,57 +79,57 @@
|
|||||||
|
|
||||||
<!-- Modal Forms -->
|
<!-- Modal Forms -->
|
||||||
<!-- Create Directory Dialog -->
|
<!-- Create Directory Dialog -->
|
||||||
<div class="modal fade" id="createDirModal" tabindex="-1" aria-labelledby="createDirModalLabel" aria-hidden="true">
|
<dialog class="modal" id="createDirModal" tabindex="-1" aria-labelledby="createDirModalLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="createDirModalLabel">Create New Directory</h5>
|
<h5 class="modal-title" id="createDirModalLabel">Create New Directory</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="dirName" class="form-label">Directory Name</label>
|
<label for="dirName" class="form-label">Directory Name</label>
|
||||||
<input type="text" class="form-control" id="dirName" placeholder="Enter directory name">
|
<input type="text" class="form-control" id="dirName" autofocus placeholder="Enter directory name">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
|
||||||
<button type="button" class="btn btn-primary" onclick="createDirectory()">Create</button>
|
<button type="button" class="btn btn-primary" onclick="createDirectory()">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dialog>
|
||||||
|
|
||||||
<!-- Create File Dialog -->
|
<!-- Create File Dialog -->
|
||||||
<div class="modal fade" id="createFileModal" tabindex="-1" aria-labelledby="createFileModalLabel" aria-hidden="true">
|
<dialog class="modal" id="createFileModal" tabindex="-1" aria-labelledby="createFileModalLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="createFileModalLabel">Create New File</h5>
|
<h5 class="modal-title" id="createFileModalLabel">Create New File</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="fileName" class="form-label">File Name (with extension)</label>
|
<label for="fileName" class="form-label">File Name (with extension)</label>
|
||||||
<input type="text" class="form-control" id="fileName" placeholder="example.txt">
|
<input type="text" class="form-control" id="fileName" autofocus placeholder="example.txt">
|
||||||
<div class="form-text">Supported extensions: .txt, .html, .css, .js, .php, etc.</div>
|
<div class="form-text">Supported extensions: .txt, .html, .css, .js, .php, etc.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
|
||||||
<button type="button" class="btn btn-primary" onclick="createFile()">Create</button>
|
<button type="button" class="btn btn-primary" onclick="createFile()">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dialog>
|
||||||
|
|
||||||
<!-- Rename Confirmation Dialog -->
|
<!-- Rename Confirmation Dialog -->
|
||||||
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
|
<dialog class="modal" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="renameModalLabel">Rename Item</h5>
|
<h5 class="modal-title" id="renameModalLabel">Rename Item</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="rename.php">
|
<form method="POST" action="rename.php">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -149,31 +141,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="newName" class="form-label">New Name:</label>
|
<label for="newName" class="form-label">New Name:</label>
|
||||||
<input type="text" class="form-control" id="newName" name="newName" required>
|
<input type="text" class="form-control" id="newName" autofocus name="newName" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Rename</button>
|
<button type="submit" class="btn btn-primary">Rename</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dialog>
|
||||||
<!-- End of Modal Forms -->
|
<!-- End of Modal Forms -->
|
||||||
|
|
||||||
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) { ?>
|
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) { ?>
|
||||||
<div class="dropzone" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
|
<div class="dropzone" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
|
||||||
Drop files or folders here to upload
|
<span class="upload-symbol" aria-hidden="true">↑</span><span><strong>Drop files or folders here</strong><small>Or use the upload buttons to browse your device</small></span>
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<table>
|
<div class="file-table-scroll"><table class="file-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th scope="col">Name</th>
|
||||||
<th>Size</th>
|
<th scope="col">Size</th>
|
||||||
<th>Actions</th>
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -197,31 +189,30 @@
|
|||||||
|
|
||||||
if ($currentPath !== $defaultPath) : ?>
|
if ($currentPath !== $defaultPath) : ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3"><a class="text-danger" href="?path=<?= urlencode(dirname($currentPath)) ?>"><b>.. (Go Back)</b></a></td>
|
<td colspan="3"><a class="text-danger" href="?path=<?= urlencode(remoteDirname($currentPath)) ?>"><b>↑ Parent folder</b></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif;
|
<?php endif;
|
||||||
|
|
||||||
foreach ($directories as $directory) : ?>
|
foreach ($directories as $directory) : ?>
|
||||||
<tr>
|
<tr data-file-name="<?= htmlspecialchars($directory) ?>" data-kind="folder">
|
||||||
<td colspan="2">
|
<td><span class="file-icon folder-icon" aria-hidden="true">▰</span>
|
||||||
<a href="?path=<?= urlencode($currentPath . '/' . $directory) ?>"><?= htmlspecialchars($directory) ?>/</a>
|
<a href="?path=<?= urlencode(rtrim($currentPath, '/') . '/' . $directory) ?>"><?= htmlspecialchars($directory) ?></a></td><td class="size-cell">—</td>
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<?php if (isset($_SESSION["delPer"]) && $_SESSION["delPer"] == true) : ?>
|
<?php if (isset($_SESSION["delPer"]) && $_SESSION["delPer"] == true) : ?>
|
||||||
<form method="POST" action="delete.php" style="display:inline;">
|
<form method="POST" action="delete.php" style="display:inline;">
|
||||||
<input type="hidden" name="delete" value="<?= htmlspecialchars($currentPath . '/' . $directory) ?>">
|
<input type="hidden" name="delete" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $directory) ?>">
|
||||||
<button type="submit" class="btn btn-danger mt-1 mb-1" onclick="confirmDelete(event)">Delete</button>
|
<button type="submit" class="btn btn-danger mt-1 mb-1" onclick="confirmDelete(event)">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
|
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
|
||||||
<form method="POST" action="download.php" style="display:inline;">
|
<form method="POST" action="download.php" style="display:inline;">
|
||||||
<input type="hidden" name="file" value="<?= htmlspecialchars($currentPath . '/' . $directory) ?>">
|
<input type="hidden" name="file" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $directory) ?>">
|
||||||
<button type="submit" class="btn btn-success mt-1 mb-1">Download</button>
|
<button type="submit" class="btn btn-success mt-1 mb-1">Download</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
|
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
|
||||||
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($directory)) ?>', '<?= htmlspecialchars(addslashes($currentPath . '/' . $directory)) ?>')">Rename</button>
|
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($directory)) ?>', '<?= htmlspecialchars(addslashes(rtrim($currentPath, '/') . '/' . $directory)) ?>', true)">Rename</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -229,7 +220,7 @@
|
|||||||
|
|
||||||
foreach ($files as $file) : ?>
|
foreach ($files as $file) : ?>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
$fileExtension = pathinfo($file, PATHINFO_EXTENSION);
|
$fileExtension = pathinfo($file, PATHINFO_EXTENSION);
|
||||||
$isPDF = strtolower($fileExtension) === 'pdf';
|
$isPDF = strtolower($fileExtension) === 'pdf';
|
||||||
$isImage = in_array($fileExtension, $imageExtensions);
|
$isImage = in_array($fileExtension, $imageExtensions);
|
||||||
@@ -242,11 +233,10 @@
|
|||||||
$isArchive = true;
|
$isArchive = true;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr data-file-name="<?= htmlspecialchars($file) ?>" data-kind="file">
|
||||||
<td>
|
<td><span class="file-icon" aria-hidden="true">▤</span>
|
||||||
|
|
||||||
<?php if ($isMedia): ?>
|
<?php if ($isMedia): ?>
|
||||||
<a class="text-info-emphasis" href="view.php?file=<?= urlencode($currentPath . '/' . $file) ?>">
|
<a class="text-info-emphasis" href="view.php?file=<?= urlencode(rtrim($currentPath, '/') . '/' . $file) ?>">
|
||||||
<?= htmlspecialchars($file) ?>
|
<?= htmlspecialchars($file) ?>
|
||||||
</a>
|
</a>
|
||||||
<?php if ($isImage): ?>
|
<?php if ($isImage): ?>
|
||||||
@@ -257,12 +247,12 @@
|
|||||||
<span class="badge bg-info rounded-pill">Audio</span>
|
<span class="badge bg-info rounded-pill">Audio</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php elseif ($isEditable): ?>
|
<?php elseif ($isEditable): ?>
|
||||||
<a class="text-warning-emphasis" href="open.php?file=<?= urlencode($currentPath . '/' . $file) ?>">
|
<a class="text-warning-emphasis" href="open.php?file=<?= urlencode(rtrim($currentPath, '/') . '/' . $file) ?>">
|
||||||
<?= htmlspecialchars($file) ?>
|
<?= htmlspecialchars($file) ?>
|
||||||
</a>
|
</a>
|
||||||
<span class="badge bg-secondary rounded-pill"><?= htmlspecialchars($fileExtension) ?></span>
|
<span class="badge bg-secondary rounded-pill"><?= htmlspecialchars($fileExtension) ?></span>
|
||||||
<?php elseif ($isPDF): ?>
|
<?php elseif ($isPDF): ?>
|
||||||
<a class="text-info-emphasis" href="pdf.php?file=<?= urlencode($currentPath . '/' . $file) ?>&type=pdf">
|
<a class="text-info-emphasis" href="pdf.php?file=<?= urlencode(rtrim($currentPath, '/') . '/' . $file) ?>&type=pdf">
|
||||||
<?= htmlspecialchars($file) ?>
|
<?= htmlspecialchars($file) ?>
|
||||||
</a>
|
</a>
|
||||||
<span class="badge bg-danger rounded-pill">PDF</span>
|
<span class="badge bg-danger rounded-pill">PDF</span>
|
||||||
@@ -272,28 +262,28 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="text"><?= formatBytes($sftp->stat($currentPath . '/' . $file)['size']) ?></span>
|
<span class="text"><?= formatBytes($sftp->stat(rtrim($currentPath, '/') . '/' . $file)['size']) ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if (isset($_SESSION["delPer"]) && $_SESSION["delPer"] == true) : ?>
|
<?php if (isset($_SESSION["delPer"]) && $_SESSION["delPer"] == true) : ?>
|
||||||
<form method="POST" action="delete.php" style="display:inline;">
|
<form method="POST" action="delete.php" style="display:inline;">
|
||||||
<input type="hidden" name="delete" value="<?= htmlspecialchars($currentPath . '/' . $file) ?>">
|
<input type="hidden" name="delete" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $file) ?>">
|
||||||
<button type="submit" class="btn btn-danger mt-1 mb-1" onclick="confirmDelete(event)">Delete</button>
|
<button type="submit" class="btn btn-danger mt-1 mb-1" onclick="confirmDelete(event)">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
|
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
|
||||||
<form method="POST" action="download.php" style="display:inline;">
|
<form method="POST" action="download.php" style="display:inline;">
|
||||||
<input type="hidden" name="file" value="<?= htmlspecialchars($currentPath . '/' . $file) ?>">
|
<input type="hidden" name="file" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $file) ?>">
|
||||||
<button type="submit" class="btn btn-success mt-1 mb-1">Download</button>
|
<button type="submit" class="btn btn-success mt-1 mb-1">Download</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
|
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
|
||||||
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($file)) ?>', '<?= htmlspecialchars(addslashes($currentPath . '/' . $file)) ?>')">Rename</button>
|
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($file)) ?>', '<?= htmlspecialchars(addslashes(rtrim($currentPath, '/') . '/' . $file)) ?>')">Rename</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($isArchive && isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
|
<?php if ($isArchive && isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
|
||||||
<form method="POST" action="extract.php" style="display:inline;">
|
<form method="POST" action="extract.php" style="display:inline;">
|
||||||
<input type="hidden" name="archive" value="<?= htmlspecialchars($currentPath . '/' . $file) ?>">
|
<input type="hidden" name="archive" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $file) ?>">
|
||||||
<button type="submit" class="btn btn-info mt-1 mb-1">Extract</button>
|
<button type="submit" class="btn btn-info mt-1 mb-1">Extract</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -301,36 +291,18 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table></div>
|
||||||
|
<div id="emptySearch" class="empty-state" hidden><span aria-hidden="true">⌕</span><h3>No files to show</h3><p>Try another search or upload files to this folder.</p></div>
|
||||||
|
<div class="browser-status"><span id="fileCount" role="status"></span><span>Folders first · A–Z</span></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</section>
|
||||||
|
<?php renderWorkspaceEnd(); ?>
|
||||||
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
|
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
// Keep Bootstrap modals outside the filtered SFTP container. A parent
|
|
||||||
// backdrop-filter creates a stacking context that can block modal input.
|
|
||||||
document.querySelectorAll('.modal').forEach(function (modal) {
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
<script>
|
||||||
// Pass PHP variables to JavaScript
|
// Pass PHP variables to JavaScript
|
||||||
var currentPath = "<?php echo $currentPath; ?>";
|
const currentPath = <?= json_encode($currentPath, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
</script>
|
</script>
|
||||||
<script src="js/index.js"></script>
|
<script src="<?= workspaceAsset('../../', 'content/ftp/js/index.js') ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+32
-47
@@ -158,6 +158,10 @@ function uploadFiles(files) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uploadStatus.setAttribute('role', 'status');
|
||||||
|
xhr.upload.addEventListener('progress', event => {
|
||||||
|
if (event.lengthComputable) uploadStatus.textContent = 'Uploading ' + filesAdded + ' file(s) · ' + Math.round(event.loaded / event.total * 100) + '%';
|
||||||
|
});
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +182,7 @@ function createDirectory() {
|
|||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
alert('Directory created successfully!');
|
alert('Directory created successfully!');
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createDirModal'));
|
document.getElementById('createDirModal').close();
|
||||||
if (modal) modal.hide();
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to create directory: ' + xhr.responseText || xhr.statusText);
|
alert('Failed to create directory: ' + xhr.responseText || xhr.statusText);
|
||||||
@@ -221,8 +224,7 @@ function createFile() {
|
|||||||
const response = JSON.parse(xhr.responseText);
|
const response = JSON.parse(xhr.responseText);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
alert('File created successfully!');
|
alert('File created successfully!');
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createFileModal'));
|
document.getElementById('createFileModal').close();
|
||||||
if (modal) modal.hide();
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to create file: ' + (response.message || 'Unknown error'));
|
alert('Failed to create file: ' + (response.message || 'Unknown error'));
|
||||||
@@ -236,53 +238,36 @@ function createFile() {
|
|||||||
};
|
};
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}
|
}
|
||||||
function openRenameModal(name, path) {
|
function openRenameModal(name, path, isDirectory = false) {
|
||||||
document.getElementById('originalName').value = name;
|
document.getElementById('originalName').value = name;
|
||||||
document.getElementById('fullPath').value = path;
|
document.getElementById('fullPath').value = path;
|
||||||
document.getElementById('isDirectory').value = name.endsWith('/') ? '1' : '0';
|
document.getElementById('isDirectory').value = isDirectory ? '1' : '0';
|
||||||
document.getElementById('newName').value = name.endsWith('/') ? name.slice(0, -1) : name;
|
document.getElementById('newName').value = name.endsWith('/') ? name.slice(0, -1) : name;
|
||||||
|
|
||||||
const renameModal = new bootstrap.Modal(document.getElementById('renameModal'));
|
document.getElementById('renameModal').showModal();
|
||||||
renameModal.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const themeToggle = document.getElementById('themeToggle');
|
// Filtering is local to the current directory; no remote request is needed.
|
||||||
const html = document.documentElement;
|
const search = document.getElementById('fileSearch');
|
||||||
const themeText = document.getElementById('themeText');
|
const typeFilter = document.getElementById('typeFilter');
|
||||||
const themeIcon = themeToggle.querySelector('.bi');
|
function filterFiles() {
|
||||||
|
const query = search.value.trim().toLocaleLowerCase();
|
||||||
function setTheme(theme) {
|
const rows = [...document.querySelectorAll('[data-file-name]')];
|
||||||
html.setAttribute('data-bs-theme', theme);
|
let visible = 0;
|
||||||
document.body.classList.remove('theme-light', 'theme-dark');
|
rows.forEach(row => {
|
||||||
document.body.classList.add('theme-' + theme);
|
row.hidden = !row.dataset.fileName.toLocaleLowerCase().includes(query) || (typeFilter.value !== 'all' && row.dataset.kind !== typeFilter.value);
|
||||||
localStorage.setItem('theme', theme);
|
if (!row.hidden) visible++;
|
||||||
|
|
||||||
if (theme === 'dark') {
|
|
||||||
themeText.textContent = 'Light Mode';
|
|
||||||
themeIcon.className = 'bi bi-sun';
|
|
||||||
themeToggle.classList.remove('btn-dark');
|
|
||||||
themeToggle.classList.add('btn-light');
|
|
||||||
} else {
|
|
||||||
themeText.textContent = 'Dark Mode';
|
|
||||||
themeIcon.className = 'bi bi-moon';
|
|
||||||
themeToggle.classList.remove('btn-light');
|
|
||||||
themeToggle.classList.add('btn-dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedTheme = localStorage.getItem('theme');
|
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
if (savedTheme) {
|
|
||||||
setTheme(savedTheme);
|
|
||||||
} else {
|
|
||||||
setTheme(prefersDark ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
themeToggle.addEventListener('click', function() {
|
|
||||||
const currentTheme = html.getAttribute('data-bs-theme');
|
|
||||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
||||||
setTheme(newTheme);
|
|
||||||
});
|
});
|
||||||
});
|
document.getElementById('fileCount').textContent = visible + ' of ' + rows.length + ' items';
|
||||||
|
document.getElementById('emptySearch').hidden = visible !== 0;
|
||||||
|
}
|
||||||
|
search?.addEventListener('input', filterFiles);
|
||||||
|
typeFilter?.addEventListener('change', filterFiles);
|
||||||
|
if (search) filterFiles();
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey && !event.target.closest('input, textarea, select, [contenteditable], dialog')) {
|
||||||
|
event.preventDefault();
|
||||||
|
search?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
// Pure parsers are kept separate so unavailable or malformed SSH output can be tested.
|
||||||
|
function getDiskData($ssh, $path) {
|
||||||
|
// The command runs on Linux even when PHP runs on Windows. escapeshellarg()
|
||||||
|
// follows the local OS, so quote explicitly for the remote POSIX shell.
|
||||||
|
$quotedPath = "'" . str_replace("'", "'\\''", $path) . "'";
|
||||||
|
return parseDiskData($ssh->exec('LC_ALL=C df -Pk -- ' . $quotedPath . ' 2>/dev/null'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemData($ssh, $storagePath) {
|
||||||
|
$meminfo = $ssh->exec("LC_ALL=C free -m | awk '/^Mem:/{printf \"%s,%s,%s,\", \$2,\$3,\$7} /^Swap:/{printf \"%s,%s,%s\", \$2,\$3,\$4}'");
|
||||||
|
$cpuIdle = $ssh->exec("LC_ALL=C vmstat 1 2 | tail -1 | awk '{print \$15}'");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'storage' => [
|
||||||
|
'system' => getDiskData($ssh, '/'),
|
||||||
|
'data' => getDiskData($ssh, $storagePath),
|
||||||
|
],
|
||||||
|
'raid' => parseRaidStatus($ssh->exec('cat /proc/mdstat 2>/dev/null')),
|
||||||
|
'resources' => parseSystemStats($cpuIdle, $meminfo),
|
||||||
|
'lastUpdate' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDiskData($output) {
|
||||||
|
$unavailable = ['available_data' => false, 'size' => 'N/A', 'used' => 'N/A', 'available' => 'N/A', 'usage' => 'N/A', 'filesystem' => ''];
|
||||||
|
if (!is_string($output)) return $unavailable;
|
||||||
|
|
||||||
|
foreach (preg_split('/\r?\n/', trim($output)) as $line) {
|
||||||
|
// POSIX df -Pk gives one filesystem per line and sizes in KiB.
|
||||||
|
if (!preg_match('/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.+)$/', trim($line), $parts)) continue;
|
||||||
|
return [
|
||||||
|
'available_data' => true,
|
||||||
|
'filesystem' => $parts[1],
|
||||||
|
'size' => formatBytes((float) $parts[2] * 1024),
|
||||||
|
'used' => formatBytes((float) $parts[3] * 1024),
|
||||||
|
'available' => formatBytes((float) $parts[4] * 1024),
|
||||||
|
'usage' => $parts[5] . '%',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $unavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRaidStatus($output) {
|
||||||
|
$status = ['active' => false, 'status' => 'unavailable', 'type' => 'N/A', 'arrays' => []];
|
||||||
|
if (!is_string($output) || trim($output) === '') return $status;
|
||||||
|
|
||||||
|
preg_match_all('/^(md\S+)\s*:\s*(active|inactive)\s+([^\r\n]+)((?:\r?\n[ \t]+[^\r\n]*)*)/m', $output, $matches, PREG_SET_ORDER);
|
||||||
|
foreach ($matches as $match) {
|
||||||
|
$details = $match[3] . $match[4];
|
||||||
|
preg_match('/\b(raid\d+|linear|multipath)\b/', $match[3], $level);
|
||||||
|
$state = $match[2] === 'active' ? 'unknown' : 'inactive';
|
||||||
|
if ($state !== 'inactive') {
|
||||||
|
if (preg_match('/\[([U_]+)\]/', $details, $members)) {
|
||||||
|
$state = strpos($members[1], '_') !== false ? 'degraded' : 'healthy';
|
||||||
|
} elseif (in_array($level[1] ?? '', ['raid0', 'linear'], true)) {
|
||||||
|
$state = 'active'; // Non-redundant arrays do not expose a [UU] health bitmap.
|
||||||
|
}
|
||||||
|
if (preg_match('/\b(recovery|resync|reshape|check|repair)\s*=/', $details)) {
|
||||||
|
if ($state !== 'degraded') $state = 'syncing';
|
||||||
|
}
|
||||||
|
if (strpos($details, '(F)') !== false) $state = 'degraded';
|
||||||
|
}
|
||||||
|
$status['arrays'][] = ['name' => $match[1], 'type' => $level[1] ?? 'N/A', 'status' => $state];
|
||||||
|
}
|
||||||
|
if (!$status['arrays']) {
|
||||||
|
$status['status'] = strpos($output, 'Personalities :') !== false ? 'not configured' : 'unavailable';
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
$states = array_column($status['arrays'], 'status');
|
||||||
|
$status['active'] = !in_array('inactive', $states, true);
|
||||||
|
$status['type'] = implode(', ', array_unique(array_column($status['arrays'], 'type')));
|
||||||
|
foreach (['inactive', 'degraded', 'unknown', 'syncing', 'active', 'healthy'] as $state) {
|
||||||
|
if (in_array($state, $states, true)) { $status['status'] = $state; break; }
|
||||||
|
}
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSystemStats($cpuOutput, $memOutput) {
|
||||||
|
$idle = is_string($cpuOutput) ? str_replace(',', '.', trim($cpuOutput)) : '';
|
||||||
|
$cpuAvailable = is_numeric($idle) && (float) $idle >= 0 && (float) $idle <= 100;
|
||||||
|
$cpu = ['available_data' => $cpuAvailable, 'usage' => $cpuAvailable ? number_format(100 - (float) $idle, 1, '.', '') : null];
|
||||||
|
$memory = array_fill_keys(['total', 'used', 'available', 'usage', 'swap_total', 'swap_used', 'swap_free', 'swap_usage'], 'N/A');
|
||||||
|
$memory['available_data'] = false;
|
||||||
|
$parts = is_string($memOutput) ? explode(',', trim($memOutput)) : [];
|
||||||
|
if (count($parts) === 6 && count(array_filter($parts, 'is_numeric')) === 6 && min(array_map('floatval', $parts)) >= 0 && (float) $parts[0] > 0) {
|
||||||
|
[$total, $used, $available, $swapTotal, $swapUsed, $swapFree] = array_map('floatval', $parts);
|
||||||
|
$memory = [
|
||||||
|
'available_data' => true,
|
||||||
|
'total' => formatBytes($total * 1024 * 1024),
|
||||||
|
'used' => formatBytes($used * 1024 * 1024),
|
||||||
|
'available' => formatBytes($available * 1024 * 1024),
|
||||||
|
'usage' => min(100, round($used / $total * 100, 1)) . '%',
|
||||||
|
'swap_total' => formatBytes($swapTotal * 1024 * 1024),
|
||||||
|
'swap_used' => formatBytes($swapUsed * 1024 * 1024),
|
||||||
|
'swap_free' => formatBytes($swapFree * 1024 * 1024),
|
||||||
|
'swap_usage' => ($swapTotal > 0 ? min(100, round($swapUsed / $swapTotal * 100, 1)) : 0) . '%',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ['cpu' => $cpu, 'memory' => $memory];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes($bytes) {
|
||||||
|
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||||
|
$bytes = max((float) $bytes, 0);
|
||||||
|
$power = min($bytes > 0 ? (int) floor(log($bytes, 1024)) : 0, count($units) - 1);
|
||||||
|
return round($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
|
||||||
|
}
|
||||||
+41
-40
@@ -13,6 +13,7 @@ if(!isset($_SESSION['uname'])){
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../includes/workspace.php';
|
||||||
$sftp = initializeSFTP();
|
$sftp = initializeSFTP();
|
||||||
|
|
||||||
$filePath = isset($_GET['file']) ? $_GET['file'] : '';
|
$filePath = isset($_GET['file']) ? $_GET['file'] : '';
|
||||||
@@ -37,7 +38,7 @@ if (!empty($filePath) && $sftp->file_exists($filePath) && !$sftp->is_dir($filePa
|
|||||||
$saveError = "Failed to save changes. Check permissions.";
|
$saveError = "Failed to save changes. Check permissions.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = $sftp->get($filePath);
|
$content = $sftp->get($filePath);
|
||||||
if ($content === false) {
|
if ($content === false) {
|
||||||
$error = "Failed to read file contents.";
|
$error = "Failed to read file contents.";
|
||||||
@@ -51,45 +52,44 @@ if (!empty($filePath) && $sftp->file_exists($filePath) && !$sftp->is_dir($filePa
|
|||||||
|
|
||||||
$showLineNumbers = in_array(strtolower($extension), ['php', 'js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'h', 'hpp', 'rb', 'sh', 'xml', 'json']);
|
$showLineNumbers = in_array(strtolower($extension), ['php', 'js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'h', 'hpp', 'rb', 'sh', 'xml', 'json']);
|
||||||
|
|
||||||
$parentDir = dirname($filePath);
|
$parentDir = remoteDirname($filePath);
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Edit File - <?= htmlspecialchars($fileName) ?></title>
|
<title>Edit File - <?= htmlspecialchars($fileName) ?></title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="../../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/open.css') ?>">
|
||||||
<link rel="stylesheet" href="css/open.css">
|
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
|
||||||
<script src="../../js/bootstrap.bundle.js"></script>
|
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'files',
|
||||||
<i class="bi"></i>
|
'section' => 'File editor',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'File editor',
|
||||||
</button>
|
'title' => $fileName,
|
||||||
</div>
|
'description' => 'View and edit text files in your remote workspace.',
|
||||||
<div class="custom-container dark-box">
|
'connected' => true,
|
||||||
<div class="header-container">
|
'back' => 'index.php?path=' . urlencode($parentDir),
|
||||||
<h1>Edit File: <?= htmlspecialchars($fileName) ?></h1>
|
'class' => 'editor-page',
|
||||||
<a href="index.php?path=<?= urlencode($parentDir) ?>" class="btn btn-secondary">Back to File List</a>
|
]); ?>
|
||||||
</div>
|
<section class="content-panel">
|
||||||
|
|
||||||
<?php if (isset($error)): ?>
|
<?php if (isset($error)): ?>
|
||||||
<div class="alert alert-danger"><?= $error ?></div>
|
<div class="alert alert-danger"><?= $error ?></div>
|
||||||
<?php elseif ($editable): ?>
|
<?php elseif ($editable): ?>
|
||||||
<?php if (isset($saveSuccess)): ?>
|
<?php if (isset($saveSuccess)): ?>
|
||||||
<div class="alert alert-success">File saved successfully!</div>
|
<div class="alert alert-success">File saved successfully!</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (isset($saveError)): ?>
|
<?php if (isset($saveError)): ?>
|
||||||
<div class="alert alert-danger"><?= $saveError ?></div>
|
<div class="alert alert-danger"><?= $saveError ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="POST" id="editorForm">
|
<form method="POST" id="editorForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
@@ -105,7 +105,7 @@ $parentDir = dirname($filePath);
|
|||||||
<textarea id="editor" name="content" class="form-control" <?= (!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true) ? 'readonly' : '' ?>><?= htmlspecialchars($content) ?></textarea>
|
<textarea id="editor" name="content" class="form-control" <?= (!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true) ? 'readonly' : '' ?>><?= htmlspecialchars($content) ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true): ?>
|
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true): ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
@@ -113,54 +113,55 @@ $parentDir = dirname($filePath);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</section>
|
||||||
<script src="../../js/theme.js"></script>
|
<?php renderWorkspaceEnd(); ?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function updateLineNumbers() {
|
function updateLineNumbers() {
|
||||||
const editor = document.getElementById('editor');
|
const editor = document.getElementById('editor');
|
||||||
const lineNumbers = document.getElementById('lineNumbers');
|
const lineNumbers = document.getElementById('lineNumbers');
|
||||||
|
|
||||||
if (!lineNumbers) return;
|
if (!lineNumbers) return;
|
||||||
|
|
||||||
const lines = editor.value.split('\n');
|
const lines = editor.value.split('\n');
|
||||||
const lineCount = lines.length;
|
const lineCount = lines.length;
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
for (let i = 1; i <= lineCount; i++) {
|
for (let i = 1; i <= lineCount; i++) {
|
||||||
html += i + '<br>';
|
html += i + '<br>';
|
||||||
}
|
}
|
||||||
|
|
||||||
lineNumbers.innerHTML = html;
|
lineNumbers.innerHTML = html;
|
||||||
|
|
||||||
syncLineNumbersHeight();
|
syncLineNumbersHeight();
|
||||||
lineNumbers.scrollTop = editor.scrollTop;
|
lineNumbers.scrollTop = editor.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncLineNumbersHeight() {
|
function syncLineNumbersHeight() {
|
||||||
const editor = document.getElementById('editor');
|
const editor = document.getElementById('editor');
|
||||||
const lineNumbers = document.getElementById('lineNumbers');
|
const lineNumbers = document.getElementById('lineNumbers');
|
||||||
|
|
||||||
if (!lineNumbers || !editor) return;
|
if (!lineNumbers || !editor) return;
|
||||||
|
|
||||||
lineNumbers.style.height = editor.clientHeight + 'px';
|
lineNumbers.style.height = editor.clientHeight + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const editor = document.getElementById('editor');
|
const editor = document.getElementById('editor');
|
||||||
const lineNumbers = document.getElementById('lineNumbers');
|
const lineNumbers = document.getElementById('lineNumbers');
|
||||||
|
|
||||||
if (editor && lineNumbers) {
|
if (editor && lineNumbers) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateLineNumbers();
|
updateLineNumbers();
|
||||||
syncLineNumbersHeight();
|
syncLineNumbersHeight();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
editor.addEventListener('input', updateLineNumbers);
|
editor.addEventListener('input', updateLineNumbers);
|
||||||
|
|
||||||
editor.addEventListener('keydown', function(e) {
|
editor.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const start = this.selectionStart;
|
const start = this.selectionStart;
|
||||||
const end = this.selectionEnd;
|
const end = this.selectionEnd;
|
||||||
|
|
||||||
@@ -183,10 +184,10 @@ $parentDir = dirname($filePath);
|
|||||||
const observer = new MutationObserver(function(mutations) {
|
const observer = new MutationObserver(function(mutations) {
|
||||||
syncLineNumbersHeight();
|
syncLineNumbersHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(editor, {
|
observer.observe(editor, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['style']
|
attributeFilter: ['style']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof ResizeObserver === 'function') {
|
if (typeof ResizeObserver === 'function') {
|
||||||
|
|||||||
+75
-84
@@ -8,6 +8,7 @@ if(!isset($_SESSION['uname'])){
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../includes/workspace.php';
|
||||||
$sftp = initializeSFTP();
|
$sftp = initializeSFTP();
|
||||||
|
|
||||||
if (!isset($_GET['file'])) {
|
if (!isset($_GET['file'])) {
|
||||||
@@ -38,11 +39,11 @@ if (isset($_GET['stream'])) {
|
|||||||
$matches = [];
|
$matches = [];
|
||||||
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
|
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
|
||||||
$start = intval($matches[1]);
|
$start = intval($matches[1]);
|
||||||
|
|
||||||
if (!empty($matches[2])) {
|
if (!empty($matches[2])) {
|
||||||
$end = intval($matches[2]);
|
$end = intval($matches[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$length = $end - $start + 1;
|
$length = $end - $start + 1;
|
||||||
|
|
||||||
header('HTTP/1.1 206 Partial Content');
|
header('HTTP/1.1 206 Partial Content');
|
||||||
@@ -77,13 +78,13 @@ if (isset($_GET['stream'])) {
|
|||||||
$readSize = min($chunkSize, $bytesRemaining);
|
$readSize = min($chunkSize, $bytesRemaining);
|
||||||
|
|
||||||
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
||||||
|
|
||||||
if ($chunkData !== false) {
|
if ($chunkData !== false) {
|
||||||
$bytesSent = strlen($chunkData);
|
$bytesSent = strlen($chunkData);
|
||||||
echo $chunkData;
|
echo $chunkData;
|
||||||
$bytesRemaining -= $bytesSent;
|
$bytesRemaining -= $bytesSent;
|
||||||
$currentPosition += $bytesSent;
|
$currentPosition += $bytesSent;
|
||||||
|
|
||||||
if (ob_get_level()) {
|
if (ob_get_level()) {
|
||||||
ob_flush();
|
ob_flush();
|
||||||
}
|
}
|
||||||
@@ -111,35 +112,34 @@ if (isset($_GET['stream'])) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PDF Viewer</title>
|
<title>PDF Viewer</title>
|
||||||
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="stylesheet" href="../../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/pdf.css') ?>">
|
||||||
<link rel="stylesheet" href="css/pdf.css">
|
|
||||||
<script src="../../js/bootstrap.bundle.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.js"></script>
|
||||||
|
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
|
||||||
|
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'files',
|
||||||
<i class="bi"></i>
|
'section' => 'PDF viewer',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Document preview',
|
||||||
</button>
|
'title' => $fileName,
|
||||||
</div>
|
'description' => 'Browse your document without leaving the workspace.',
|
||||||
<div class="custom-container dark-box">
|
'connected' => true,
|
||||||
<header class="text-center border-bottom m-5">
|
'back' => 'index.php?path=' . urlencode(remoteDirname($filePath)),
|
||||||
<h1 class="mb-4">PDF Viewer</h1>
|
'class' => 'pdf-page',
|
||||||
<div class="mb-3">
|
]); ?>
|
||||||
<a href="index.php?path=<?= urlencode(dirname($filePath)) ?>" class="btn btn-primary">Back to Files</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
|
|
||||||
|
<section class="content-panel pdf-workspace" aria-label="PDF document">
|
||||||
<div id="pdfContainer" class="container-fluid position-relative">
|
<div id="pdfContainer" class="container-fluid position-relative">
|
||||||
<div id="loadingMessage" class="d-flex align-items-center justify-content-center w-100 h-100">
|
<div id="loadingMessage" class="d-flex align-items-center justify-content-center w-100 h-100">
|
||||||
<div class="bg-dark bg-opacity-75 text-white p-3 rounded">
|
<div class="bg-dark bg-opacity-75 text-white p-3 rounded">
|
||||||
@@ -151,17 +151,17 @@ if (isset($_GET['stream'])) {
|
|||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="btn-group" role="group" aria-label="PDF Navigation">
|
<div class="btn-group" role="group" aria-label="PDF Navigation">
|
||||||
<button id="prev" class="btn btn-light border">
|
<button id="prev" class="btn btn-light border" disabled>
|
||||||
<i class="bi bi-chevron-left"></i> Previous
|
<i class="ui-icon icon-chevron-left"></i> Previous
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-light border disabled">
|
<span class="btn btn-light border" role="status">
|
||||||
Page <span id="pageNum"></span> of <span id="pageCount"></span>
|
Page <span id="pageNum"></span> of <span id="pageCount"></span>
|
||||||
|
</span>
|
||||||
|
<button id="next" class="btn btn-light border" disabled>
|
||||||
|
Next <i class="ui-icon icon-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="next" class="btn btn-light border">
|
<button id="fullscreen" aria-label="Toggle fullscreen" class="btn btn-light border">
|
||||||
Next <i class="bi bi-chevron-right"></i>
|
<i class="ui-icon icon-fullscreen"></i>
|
||||||
</button>
|
|
||||||
<button id="fullscreen" class="btn btn-light border">
|
|
||||||
<i class="bi bi-fullscreen"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,26 +171,14 @@ if (isset($_GET['stream'])) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
|
</section>
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
<?php renderWorkspaceEnd(); ?>
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
|
|
||||||
const url = 'pdf.php?file=<?= urlencode($filePath) ?>&stream=1';
|
const url = 'pdf.php?file=<?= urlencode($filePath) ?>&stream=1';
|
||||||
const pdfFileName = '<?= htmlspecialchars($fileName) ?>';
|
const pdfFileName = <?= json_encode($fileName, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let pdfDoc = null;
|
let pdfDoc = null;
|
||||||
|
|
||||||
@@ -198,46 +186,47 @@ if (isset($_GET['stream'])) {
|
|||||||
getStorageKey() {
|
getStorageKey() {
|
||||||
return `pdf_page_${pdfFileName}`;
|
return `pdf_page_${pdfFileName}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
savePage(pageNum) {
|
savePage(pageNum) {
|
||||||
localStorage.setItem(this.getStorageKey(), pageNum.toString());
|
localStorage.setItem(this.getStorageKey(), pageNum.toString());
|
||||||
},
|
},
|
||||||
|
|
||||||
loadPage() {
|
loadPage() {
|
||||||
return parseInt(localStorage.getItem(this.getStorageKey())) || 1;
|
return parseInt(localStorage.getItem(this.getStorageKey())) || 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadPDF() {
|
async function loadPDF() {
|
||||||
try {
|
try {
|
||||||
pdfDoc = await pdfjsLib.getDocument(url).promise;
|
pdfDoc = await pdfjsLib.getDocument(url).promise;
|
||||||
document.getElementById('pageCount').textContent = pdfDoc.numPages;
|
document.getElementById('pageCount').textContent = pdfDoc.numPages;
|
||||||
|
|
||||||
currentPage = Math.min(PdfPageStorage.loadPage(), pdfDoc.numPages);
|
currentPage = Math.min(PdfPageStorage.loadPage(), pdfDoc.numPages);
|
||||||
|
|
||||||
renderPage(currentPage);
|
await renderPage(currentPage);
|
||||||
document.getElementById('loadingMessage').classList.add('d-none');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
const loadingMessage = document.getElementById('loadingMessage');
|
const loadingMessage = document.getElementById('loadingMessage');
|
||||||
loadingMessage.querySelector('div').classList.add('bg-danger');
|
if (loadingMessage) {
|
||||||
loadingMessage.querySelector('div').innerHTML = '<i class="bi bi-exclamation-triangle"></i> Error loading PDF';
|
loadingMessage.querySelector('div').className = 'alert alert-danger';
|
||||||
|
loadingMessage.querySelector('div').textContent = 'Unable to load this PDF.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPage(pageNumber) {
|
async function renderPage(pageNumber) {
|
||||||
const page = await pdfDoc.getPage(pageNumber);
|
const page = await pdfDoc.getPage(pageNumber);
|
||||||
const pageContainer = document.createElement('div');
|
const pageContainer = document.createElement('div');
|
||||||
pageContainer.className = 'page-container';
|
pageContainer.className = 'page-container';
|
||||||
|
|
||||||
const windowWidth = window.innerWidth;
|
const windowWidth = document.getElementById('pdfContainer').clientWidth - 32;
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
const viewport = page.getViewport({ scale: 1.0 });
|
const viewport = page.getViewport({ scale: 1.0 });
|
||||||
|
|
||||||
const pixelRatio = window.devicePixelRatio || 1;
|
const pixelRatio = window.devicePixelRatio || 1;
|
||||||
const widthScale = (windowWidth / viewport.width);
|
const widthScale = (windowWidth / viewport.width);
|
||||||
const heightScale = (windowHeight / viewport.height);
|
const heightScale = (windowHeight / viewport.height);
|
||||||
|
|
||||||
let scale;
|
let scale;
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
scale = Math.min(widthScale, heightScale) * 0.95;
|
scale = Math.min(widthScale, heightScale) * 0.95;
|
||||||
@@ -246,28 +235,30 @@ if (isset($_GET['stream'])) {
|
|||||||
} else {
|
} else {
|
||||||
scale = Math.min(widthScale, heightScale, 2);
|
scale = Math.min(widthScale, heightScale, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaledViewport = page.getViewport({ scale: scale * pixelRatio });
|
const scaledViewport = page.getViewport({ scale: scale * pixelRatio });
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
canvas.height = scaledViewport.height;
|
canvas.height = scaledViewport.height;
|
||||||
canvas.width = scaledViewport.width;
|
canvas.width = scaledViewport.width;
|
||||||
|
|
||||||
canvas.style.width = `${scaledViewport.width / pixelRatio}px`;
|
canvas.style.width = `${scaledViewport.width / pixelRatio}px`;
|
||||||
canvas.style.height = `${scaledViewport.height / pixelRatio}px`;
|
canvas.style.height = `${scaledViewport.height / pixelRatio}px`;
|
||||||
|
|
||||||
pageContainer.appendChild(canvas);
|
pageContainer.appendChild(canvas);
|
||||||
document.getElementById('pdfContainer').innerHTML = '';
|
document.getElementById('pdfContainer').innerHTML = '';
|
||||||
document.getElementById('pdfContainer').appendChild(pageContainer);
|
document.getElementById('pdfContainer').appendChild(pageContainer);
|
||||||
document.getElementById('pageNum').textContent = pageNumber;
|
document.getElementById('pageNum').textContent = pageNumber;
|
||||||
|
document.getElementById('prev').disabled = pageNumber <= 1;
|
||||||
|
document.getElementById('next').disabled = pageNumber >= pdfDoc.numPages;
|
||||||
|
|
||||||
await page.render({
|
await page.render({
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
viewport: scaledViewport // Use scaledViewport instead of viewport
|
viewport: scaledViewport // Use scaledViewport instead of viewport
|
||||||
}).promise;
|
}).promise;
|
||||||
|
|
||||||
PdfPageStorage.savePage(pageNumber);
|
PdfPageStorage.savePage(pageNumber);
|
||||||
}
|
}
|
||||||
document.getElementById('prev').addEventListener('click', () => {
|
document.getElementById('prev').addEventListener('click', () => {
|
||||||
@@ -276,9 +267,9 @@ if (isset($_GET['stream'])) {
|
|||||||
renderPage(currentPage);
|
renderPage(currentPage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('next').addEventListener('click', () => {
|
document.getElementById('next').addEventListener('click', () => {
|
||||||
if (currentPage < pdfDoc.numPages) {
|
if (pdfDoc && currentPage < pdfDoc.numPages) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
renderPage(currentPage);
|
renderPage(currentPage);
|
||||||
}
|
}
|
||||||
@@ -290,14 +281,14 @@ if (isset($_GET['stream'])) {
|
|||||||
} else if (e.key === 'ArrowLeft' && currentPage > 1) {
|
} else if (e.key === 'ArrowLeft' && currentPage > 1) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
renderPage(currentPage);
|
renderPage(currentPage);
|
||||||
} else if (e.key === 'ArrowRight' && currentPage < pdfDoc.numPages) {
|
} else if (e.key === 'ArrowRight' && pdfDoc && currentPage < pdfDoc.numPages) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
renderPage(currentPage);
|
renderPage(currentPage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (currentPage) {
|
if (pdfDoc) {
|
||||||
renderPage(currentPage);
|
renderPage(currentPage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -305,28 +296,28 @@ if (isset($_GET['stream'])) {
|
|||||||
let isFullscreen = false;
|
let isFullscreen = false;
|
||||||
|
|
||||||
document.getElementById('fullscreen').addEventListener('click', () => {
|
document.getElementById('fullscreen').addEventListener('click', () => {
|
||||||
const container = document.querySelector('.custom-container');
|
const container = document.querySelector('.pdf-workspace');
|
||||||
const fullscreenBtn = document.getElementById('fullscreen');
|
const fullscreenBtn = document.getElementById('fullscreen');
|
||||||
const fullscreenIcon = fullscreenBtn.querySelector('i');
|
const fullscreenIcon = fullscreenBtn.querySelector('i');
|
||||||
|
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
container.classList.add('fullscreen-mode');
|
container.classList.add('fullscreen-mode');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
fullscreenIcon.classList.remove('bi-fullscreen');
|
fullscreenIcon.classList.remove('icon-fullscreen');
|
||||||
fullscreenIcon.classList.add('bi-fullscreen-exit');
|
fullscreenIcon.classList.add('icon-fullscreen-exit');
|
||||||
} else {
|
} else {
|
||||||
container.classList.remove('fullscreen-mode');
|
container.classList.remove('fullscreen-mode');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
fullscreenIcon.classList.remove('bi-fullscreen-exit');
|
fullscreenIcon.classList.remove('icon-fullscreen-exit');
|
||||||
fullscreenIcon.classList.add('bi-fullscreen');
|
fullscreenIcon.classList.add('icon-fullscreen');
|
||||||
}
|
}
|
||||||
|
|
||||||
isFullscreen = !isFullscreen;
|
isFullscreen = !isFullscreen;
|
||||||
renderPage(currentPage);
|
if (pdfDoc) renderPage(currentPage);
|
||||||
});
|
});
|
||||||
|
|
||||||
loadPDF();
|
loadPDF();
|
||||||
</script>
|
</script>
|
||||||
<script src="../../js/theme.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
die("Missing required information for renaming.");
|
die("Missing required information for renaming.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$dirPath = dirname($oldPath);
|
$dirPath = remoteDirname($oldPath);
|
||||||
$oldName = basename($oldPath);
|
$oldName = basename($oldPath);
|
||||||
|
|
||||||
$newPath = $dirPath . '/' . $newName;
|
$newPath = $dirPath . '/' . $newName;
|
||||||
|
|||||||
+103
-377
@@ -12,47 +12,20 @@ if (empty($_SESSION['admin']) || $_SESSION['admin'] !== true) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../includes/workspace.php';
|
||||||
use phpseclib3\Net\SSH2;
|
|
||||||
|
|
||||||
// $ssh_host = 'localhost';
|
|
||||||
// $ssh_username = 'uname';
|
|
||||||
// $ssh_password = 'pswd';
|
|
||||||
|
|
||||||
|
// The configuration helper authenticates using SSH_HOST, SSH_USER and SSH_PASS.
|
||||||
$ssh = getSSHConnection();
|
$ssh = getSSHConnection();
|
||||||
if (!$ssh->login($ssh_username, $ssh_password)) {
|
|
||||||
die('SSH login failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDiskData($ssh, $device) {
|
require_once __DIR__ . '/lib/serverstats.php';
|
||||||
return $ssh->exec("df -h $device");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemData($ssh) {
|
$systemData = getSystemData($ssh, $defaultPath);
|
||||||
// Get memory info using awk to clean up the output
|
|
||||||
$meminfo = $ssh->exec("free -m | awk 'NR==2{printf \"%s,%s,%s,\", $2,$3,$7} NR==3{printf \"%s,%s,%s\", $2,$3,$4}'");
|
|
||||||
|
|
||||||
// Use vmstat to get CPU idle percentage, more reliable and commonly available
|
|
||||||
$cpuIdle = $ssh->exec("vmstat 1 2 | tail -1 | awk '{print $15}'");
|
|
||||||
|
|
||||||
return [
|
|
||||||
'storage' => [
|
|
||||||
'system' => parseDiskData($ssh->exec('df -h /dev/ubuntu-vg/ubuntu-lv')),
|
|
||||||
'data' => parseDiskData($ssh->exec('df -h /dev/sdb1'))
|
|
||||||
],
|
|
||||||
'raid' => parseRaidStatus($ssh->exec('cat /proc/mdstat')),
|
|
||||||
'resources' => parseSystemStats($cpuIdle, $meminfo),
|
|
||||||
'lastUpdate' => date('Y-m-d H:i:s')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$systemData = getSystemData($ssh);
|
|
||||||
|
|
||||||
$monitors = [
|
$monitors = [
|
||||||
[
|
[
|
||||||
'id' => 'SystemDisk',
|
'id' => 'SystemDisk',
|
||||||
'title' => 'System Disk (sda)',
|
'title' => 'System filesystem',
|
||||||
'icon' => 'hdd',
|
'icon' => 'hdd',
|
||||||
'color' => 'primary',
|
'color' => 'primary',
|
||||||
'type' => 'storage',
|
'type' => 'storage',
|
||||||
@@ -60,7 +33,7 @@ $monitors = [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => 'DataDisk',
|
'id' => 'DataDisk',
|
||||||
'title' => 'Data Disk (sdb)',
|
'title' => 'Storage filesystem',
|
||||||
'icon' => 'hdd',
|
'icon' => 'hdd',
|
||||||
'color' => 'primary',
|
'color' => 'primary',
|
||||||
'type' => 'storage',
|
'type' => 'storage',
|
||||||
@@ -91,349 +64,101 @@ $monitors = [
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
function parseDiskData($output) {
|
|
||||||
$lines = explode("\n", trim($output));
|
|
||||||
if (count($lines) < 2) return null;
|
|
||||||
|
|
||||||
$values = preg_split('/\s+/', trim($lines[1]));
|
|
||||||
return [
|
|
||||||
'size' => $values[1] ?? 'N/A',
|
|
||||||
'used' => $values[2] ?? 'N/A',
|
|
||||||
'available' => $values[3] ?? 'N/A',
|
|
||||||
'usage' => $values[4] ?? '0%'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRaidStatus($output) {
|
|
||||||
if (empty($output)) {
|
|
||||||
return [
|
|
||||||
'active' => false,
|
|
||||||
'status' => 'unknown',
|
|
||||||
'type' => 'N/A'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = [
|
|
||||||
'active' => false,
|
|
||||||
'status' => 'unknown',
|
|
||||||
'type' => 'N/A'
|
|
||||||
];
|
|
||||||
|
|
||||||
$lines = explode("\n", trim($output));
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (preg_match('/active\s+(\w+)/', $line, $matches)) {
|
|
||||||
$status['active'] = true;
|
|
||||||
$status['type'] = $matches[1] ?? 'N/A';
|
|
||||||
}
|
|
||||||
if (strpos($line, '[UU]') !== false) {
|
|
||||||
$status['status'] = 'healthy';
|
|
||||||
} elseif (strpos($line, '_') !== false) {
|
|
||||||
$status['status'] = 'degraded';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSystemStats($cpuOutput, $memOutput) {
|
|
||||||
// Parse CPU stats - convert idle percentage to usage percentage
|
|
||||||
$idlePercent = floatval(trim(str_replace(',', '.', $cpuOutput)));
|
|
||||||
$cpuUsage = 100 - $idlePercent; // Convert idle to usage percentage
|
|
||||||
if (is_nan($cpuUsage) || $cpuUsage < 0 || $cpuUsage > 100) {
|
|
||||||
$cpuUsage = 0; // Default to 0% if parsing fails
|
|
||||||
}
|
|
||||||
$cpu = ['usage' => number_format($cpuUsage, 1)];
|
|
||||||
|
|
||||||
// Parse memory stats
|
|
||||||
$parts = explode(',', $memOutput);
|
|
||||||
if (count($parts) >= 6) {
|
|
||||||
$memTotal = intval($parts[0]);
|
|
||||||
$memUsed = intval($parts[1]);
|
|
||||||
$memAvail = intval($parts[2]);
|
|
||||||
$swapTotal = intval($parts[3]);
|
|
||||||
$swapUsed = intval($parts[4]);
|
|
||||||
$swapFree = intval($parts[5]);
|
|
||||||
|
|
||||||
$memory = [
|
|
||||||
'total' => number_format($memTotal / 1024, 2) . 'G',
|
|
||||||
'used' => number_format($memUsed / 1024, 2) . 'G',
|
|
||||||
'available' => number_format($memAvail / 1024, 2) . 'G',
|
|
||||||
'usage' => ($memTotal > 0 ? round(($memUsed / $memTotal) * 100, 1) : 0) . '%',
|
|
||||||
'swap_total' => number_format($swapTotal / 1024, 2) . 'G',
|
|
||||||
'swap_used' => number_format($swapUsed / 1024, 2) . 'G',
|
|
||||||
'swap_free' => number_format($swapFree / 1024, 2) . 'G',
|
|
||||||
'swap_usage' => ($swapTotal > 0 ? round(($swapUsed / $swapTotal) * 100, 1) : 0) . '%'
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$memory = [
|
|
||||||
'total' => '0G',
|
|
||||||
'used' => '0G',
|
|
||||||
'available' => '0G',
|
|
||||||
'usage' => '0%',
|
|
||||||
'swap_total' => '0G',
|
|
||||||
'swap_used' => '0G',
|
|
||||||
'swap_free' => '0G',
|
|
||||||
'swap_usage' => '0%'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['cpu' => $cpu, 'memory' => $memory];
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToBytes($size) {
|
|
||||||
if (preg_match('/^([\d.]+)([KMGT]?)i?B?$/', trim($size), $matches)) {
|
|
||||||
$value = floatval($matches[1]);
|
|
||||||
$unit = strtoupper($matches[2]);
|
|
||||||
|
|
||||||
switch ($unit) {
|
|
||||||
case 'P': $value *= 1024;
|
|
||||||
case 'T': $value *= 1024;
|
|
||||||
case 'G': $value *= 1024;
|
|
||||||
case 'M': $value *= 1024;
|
|
||||||
case 'K': $value *= 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBytes($bytes, $forceUnit = '') {
|
|
||||||
$units = ['B', 'K', 'M', 'G', 'T', 'P'];
|
|
||||||
$bytes = max($bytes, 0);
|
|
||||||
|
|
||||||
if ($forceUnit) {
|
|
||||||
$unitIndex = array_search($forceUnit, $units);
|
|
||||||
if ($unitIndex !== false) {
|
|
||||||
$bytes /= pow(1024, $unitIndex);
|
|
||||||
return round($bytes, 2) . $forceUnit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
|
||||||
$pow = min($pow, count($units) - 1);
|
|
||||||
$bytes /= pow(1024, $pow);
|
|
||||||
|
|
||||||
return round($bytes, 2) . $units[$pow];
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Server Status</title>
|
<title>Server Status</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="../../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/serverstat.css') ?>">
|
||||||
<link rel="stylesheet" href="css/serverstat.css">
|
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
|
||||||
<script src="../../js/bootstrap.bundle.js"></script>
|
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="container-fluid text-center">
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'server',
|
||||||
<i class="bi"></i>
|
'section' => 'Server status',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Server overview',
|
||||||
</button>
|
'title' => 'Server status',
|
||||||
</div>
|
'description' => 'Keep an eye on your storage, RAID health and system resources.',
|
||||||
|
'connected' => true,
|
||||||
|
'class' => 'server-page',
|
||||||
|
]); ?>
|
||||||
|
|
||||||
<div class="custom-container dark-box">
|
|
||||||
<header class="row border-bottom m-5">
|
|
||||||
<h1>Server Status</h1>
|
|
||||||
<div class="mb-3 p-3">
|
|
||||||
<a href="../logout.php" class="btn btn-danger">Logout</a>
|
|
||||||
<a href="../changepassword.php" class="btn btn-warning">Change Password</a>
|
|
||||||
<a href="../adminpanel.php" class="btn btn-primary">Admin Panel</a>
|
|
||||||
<a href="index.php" class="btn btn-primary">SFTP</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="container py-4">
|
<div class="content-toolbar"><p>Live statistics · refreshes every 5 seconds</p><span class="badge bg-light text-dark"><i class="ui-icon icon-clock" aria-hidden="true"></i> <?= htmlspecialchars($systemData['lastUpdate']) ?></span></div>
|
||||||
<div class="row justify-content-center">
|
<div class="stats-grid">
|
||||||
<div class="col-lg-10">
|
<?php foreach ($monitors as $monitor): ?>
|
||||||
<div class="card shadow-sm bg-body-tertiary">
|
<section class="stats-cell" aria-label="<?= htmlspecialchars($monitor['title']) ?>">
|
||||||
<div class="card-header monitor-header">
|
<div class="monitor-card card h-100">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="card-header"><h3 class="mb-0"><i class="ui-icon icon-<?= $monitor['icon'] ?> text-primary me-2" aria-hidden="true"></i><?= htmlspecialchars($monitor['title']) ?></h3></div>
|
||||||
<h1 class="h4 mb-0">
|
<div class="card-body">
|
||||||
<i class="bi bi-server text-primary me-2"></i>
|
<?php if ($monitor['type'] === 'storage'):
|
||||||
Server Disk Status
|
$diskData = $systemData['storage'][$monitor['source']];
|
||||||
</h1>
|
?>
|
||||||
<span class="badge bg-light text-dark">
|
<?php if (!$diskData['available_data']): ?>
|
||||||
<i class="bi bi-clock me-1"></i>
|
<p class="text-muted" role="status">Storage statistics are unavailable for this path.</p>
|
||||||
<?= htmlspecialchars($systemData['lastUpdate']) ?>
|
<?php else:
|
||||||
</span>
|
$usagePercent = min(100, (int) rtrim($diskData['usage'], '%'));
|
||||||
</div>
|
?>
|
||||||
</div>
|
<p class="small text-muted"><?= htmlspecialchars($diskData['filesystem']) ?> · <?= htmlspecialchars($monitor['source'] === 'system' ? '/' : $defaultPath) ?></p>
|
||||||
|
<div class="d-flex justify-content-between"><span>Storage usage</span><strong><?= htmlspecialchars($diskData['usage']) ?></strong></div>
|
||||||
<div class="card-body">
|
<div class="progress mb-4"><div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>" role="progressbar" style="width:<?= $usagePercent ?>%" aria-label="Storage usage" aria-valuenow="<?= $usagePercent ?>" aria-valuemin="0" aria-valuemax="100"></div></div>
|
||||||
<div class="row g-4">
|
<div class="row text-center">
|
||||||
<?php foreach ($monitors as $monitor): ?>
|
<?php foreach (['size' => 'Total', 'used' => 'Used', 'available' => 'Free'] as $key => $label): ?>
|
||||||
<div class="col-md-6">
|
<div class="col-4"><div class="p-2 border rounded"><div class="small text-muted"><?= $label ?></div><div class="fw-bold"><?= htmlspecialchars($diskData[$key]) ?></div></div></div>
|
||||||
<div class="monitor-card card h-100 border-<?= $monitor['color'] ?>">
|
<?php endforeach; ?>
|
||||||
<div class="card-header bg-<?= $monitor['color'] ?> bg-opacity-10">
|
</div>
|
||||||
<h3 class="h5 mb-0">
|
<?php endif; ?>
|
||||||
<i class="bi bi-<?= $monitor['icon'] ?> text-<?= $monitor['color'] ?> me-2"></i>
|
<?php elseif ($monitor['type'] === 'raid'): ?>
|
||||||
<?= $monitor['title'] ?>
|
<div class="raid-summary">
|
||||||
</h3>
|
<i class="ui-icon icon-<?= $systemData['raid']['status'] === 'healthy' ? 'check-circle-fill text-success' : 'x-circle-fill text-warning' ?>" aria-hidden="true"></i>
|
||||||
</div>
|
<h4><?= $systemData['raid']['arrays'] ? htmlspecialchars($systemData['raid']['type']) . ' Array' : 'Linux MD RAID' ?></h4>
|
||||||
<div class="card-body">
|
<span class="badge"><?= htmlspecialchars(ucfirst($systemData['raid']['status'])) ?></span>
|
||||||
<?php if ($monitor['type'] === 'storage'):
|
<?php foreach ($systemData['raid']['arrays'] as $array): ?>
|
||||||
$diskData = $systemData['storage'][$monitor['source']];
|
<p class="small text-muted mt-2"><?= htmlspecialchars($array['name'] . ' · ' . $array['type'] . ' · ' . $array['status']) ?></p>
|
||||||
$usagePercent = intval(rtrim($diskData['usage'], '%'));
|
|
||||||
?>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="d-flex justify-content-between mb-1">
|
|
||||||
<span>Storage Usage</span>
|
|
||||||
<span><?= $diskData['usage'] ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="progress" style="height: 10px">
|
|
||||||
<div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: <?= $usagePercent ?>%"
|
|
||||||
aria-valuenow="<?= $usagePercent ?>"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row text-center g-2">
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Total</div>
|
|
||||||
<div class="fw-bold"><?= $diskData['size'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Used</div>
|
|
||||||
<div class="fw-bold"><?= $diskData['used'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Free</div>
|
|
||||||
<div class="fw-bold"><?= $diskData['available'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($monitor['type'] === 'raid'): ?>
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="display-4 mb-2">
|
|
||||||
<i class="bi bi-<?= $systemData['raid']['active'] ? 'check-circle-fill text-success' : 'x-circle-fill text-danger' ?>"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="h6"><?= $systemData['raid']['type'] ?> Array</h4>
|
|
||||||
<span class="badge bg-<?= $systemData['raid']['status'] === 'healthy' ? 'success' : 'warning' ?>">
|
|
||||||
<?= ucfirst($systemData['raid']['status']) ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($monitor['type'] === 'resources' && $monitor['source'] === 'cpu'): ?>
|
|
||||||
<div class="cpu-gauge">
|
|
||||||
<canvas id="cpuGauge" width="150" height="150" data-value="<?= $systemData['resources']['cpu']['usage'] ?>"></canvas>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($monitor['type'] === 'resources' && $monitor['source'] === 'memory'):
|
|
||||||
$memData = $systemData['resources']['memory'];
|
|
||||||
$usagePercent = floatval(rtrim($memData['usage'], '%'));
|
|
||||||
$swapPercent = floatval(rtrim($memData['swap_usage'], '%'));
|
|
||||||
?>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="d-flex justify-content-between mb-1">
|
|
||||||
<span>Memory Usage</span>
|
|
||||||
<span><?= $memData['usage'] ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="progress" style="height: 10px">
|
|
||||||
<div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: <?= $usagePercent ?>%"
|
|
||||||
aria-valuenow="<?= $usagePercent ?>"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row text-center g-2 mb-3">
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Total</div>
|
|
||||||
<div class="fw-bold"><?= $memData['total'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Used</div>
|
|
||||||
<div class="fw-bold"><?= $memData['used'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Available</div>
|
|
||||||
<div class="fw-bold"><?= $memData['available'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="d-flex justify-content-between mb-1">
|
|
||||||
<span>Swap Usage</span>
|
|
||||||
<span><?= $memData['swap_usage'] ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="progress" style="height: 10px">
|
|
||||||
<div class="progress-bar bg-<?= $swapPercent > 90 ? 'danger' : ($swapPercent > 75 ? 'warning' : 'success') ?>"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: <?= $swapPercent ?>%"
|
|
||||||
aria-valuenow="<?= $swapPercent ?>"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row text-center g-2">
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Total</div>
|
|
||||||
<div class="fw-bold"><?= $memData['swap_total'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Used</div>
|
|
||||||
<div class="fw-bold"><?= $memData['swap_used'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="p-2 border rounded">
|
|
||||||
<div class="small text-muted">Free</div>
|
|
||||||
<div class="fw-bold"><?= $memData['swap_free'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php elseif ($monitor['source'] === 'cpu'): ?>
|
||||||
|
<?php if (!$systemData['resources']['cpu']['available_data']): ?>
|
||||||
|
<p class="text-muted" role="status">CPU statistics are unavailable.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="cpu-gauge"><canvas id="cpuGauge" width="170" height="170" data-value="<?= $systemData['resources']['cpu']['usage'] ?>" aria-label="CPU usage <?= $systemData['resources']['cpu']['usage'] ?> percent"></canvas></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else:
|
||||||
|
$memData = $systemData['resources']['memory'];
|
||||||
|
?>
|
||||||
|
<?php if (!$memData['available_data']): ?>
|
||||||
|
<p class="text-muted" role="status">Memory statistics are unavailable.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach (['usage' => 'Memory usage', 'swap_usage' => 'Swap usage'] as $usageKey => $label):
|
||||||
|
$usagePercent = (float) rtrim($memData[$usageKey], '%');
|
||||||
|
$fields = $usageKey === 'usage' ? ['total' => 'Total', 'used' => 'Used', 'available' => 'Available'] : ['swap_total' => 'Total', 'swap_used' => 'Used', 'swap_free' => 'Free'];
|
||||||
|
?>
|
||||||
|
<div class="resource-section">
|
||||||
|
<div class="d-flex justify-content-between"><span><?= $label ?></span><strong><?= htmlspecialchars($memData[$usageKey]) ?></strong></div>
|
||||||
|
<div class="progress mb-3"><div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>" role="progressbar" style="width:<?= $usagePercent ?>%" aria-label="<?= $label ?>" aria-valuenow="<?= $usagePercent ?>" aria-valuemin="0" aria-valuemax="100"></div></div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<?php foreach ($fields as $key => $fieldLabel): ?>
|
||||||
|
<div class="col-4"><div class="p-2 border rounded"><div class="small text-muted"><?= $fieldLabel ?></div><div class="fw-bold"><?= htmlspecialchars($memData[$key]) ?></div></div></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php renderWorkspaceEnd(); ?>
|
||||||
|
|
||||||
</div>
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3">
|
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<script src="../../js/theme.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
let refreshInterval;
|
let refreshInterval;
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
@@ -441,13 +166,13 @@ let isRefreshing = false;
|
|||||||
function updateData(html) {
|
function updateData(html) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
// Update monitor cards
|
// Update monitor cards
|
||||||
document.querySelectorAll('.monitor-card').forEach((card, index) => {
|
document.querySelectorAll('.monitor-card').forEach((card, index) => {
|
||||||
const newCard = doc.querySelectorAll('.monitor-card')[index];
|
const newCard = doc.querySelectorAll('.monitor-card')[index];
|
||||||
if (newCard && card.querySelector('.card-body')) {
|
if (newCard && card.querySelector('.card-body')) {
|
||||||
card.querySelector('.card-body').innerHTML = newCard.querySelector('.card-body').innerHTML;
|
card.querySelector('.card-body').innerHTML = newCard.querySelector('.card-body').innerHTML;
|
||||||
|
|
||||||
// Redraw CPU gauge if this is the CPU card
|
// Redraw CPU gauge if this is the CPU card
|
||||||
if (card.querySelector('.cpu-gauge')) {
|
if (card.querySelector('.cpu-gauge')) {
|
||||||
const cpuValue = parseFloat(newCard.querySelector('canvas').getAttribute('data-value') || '0');
|
const cpuValue = parseFloat(newCard.querySelector('canvas').getAttribute('data-value') || '0');
|
||||||
@@ -455,7 +180,7 @@ function updateData(html) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update last update time
|
// Update last update time
|
||||||
const newLastUpdate = doc.querySelector('.badge.bg-light.text-dark')?.innerHTML;
|
const newLastUpdate = doc.querySelector('.badge.bg-light.text-dark')?.innerHTML;
|
||||||
const currentLastUpdate = document.querySelector('.badge.bg-light.text-dark');
|
const currentLastUpdate = document.querySelector('.badge.bg-light.text-dark');
|
||||||
@@ -463,11 +188,6 @@ function updateData(html) {
|
|||||||
currentLastUpdate.innerHTML = newLastUpdate;
|
currentLastUpdate.innerHTML = newLastUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update CPU gauge if needed
|
|
||||||
const cpuValue = document.querySelector('.cpu-gauge .h4')?.textContent;
|
|
||||||
if (cpuValue) {
|
|
||||||
drawCpuGauge(parseFloat(cpuValue));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRefresh() {
|
function startRefresh() {
|
||||||
@@ -476,7 +196,10 @@ function startRefresh() {
|
|||||||
if (!isRefreshing) {
|
if (!isRefreshing) {
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
fetch(window.location.href)
|
fetch(window.location.href)
|
||||||
.then(response => response.text())
|
.then(response => {
|
||||||
|
if (!response.ok || response.redirected) throw new Error('Status refresh failed');
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
.then(html => {
|
.then(html => {
|
||||||
updateData(html);
|
updateData(html);
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
@@ -506,17 +229,16 @@ document.addEventListener('visibilitychange', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initial setup
|
// Initial setup
|
||||||
startRefresh();
|
if (!document.hidden) startRefresh();
|
||||||
drawCpuGauge(parseFloat(document.querySelector('.cpu-gauge .h4')?.textContent || '0'));
|
|
||||||
|
|
||||||
function drawCpuGauge(value) {
|
function drawCpuGauge(value) {
|
||||||
const canvas = document.getElementById('cpuGauge');
|
const canvas = document.getElementById('cpuGauge');
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
// Ensure value is a valid number between 0 and 100
|
// Ensure value is a valid number between 0 and 100
|
||||||
value = parseFloat(value) || 0;
|
value = parseFloat(value) || 0;
|
||||||
value = Math.max(0, Math.min(100, value));
|
value = Math.max(0, Math.min(100, value));
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const centerX = canvas.width / 2;
|
const centerX = canvas.width / 2;
|
||||||
const centerY = canvas.height / 2;
|
const centerY = canvas.height / 2;
|
||||||
@@ -527,14 +249,14 @@ function drawCpuGauge(value) {
|
|||||||
// Draw background circle
|
// Draw background circle
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--bs-border-color');
|
ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--line');
|
||||||
ctx.lineWidth = 10;
|
ctx.lineWidth = 10;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Draw value arc
|
// Draw value arc
|
||||||
const startAngle = -Math.PI / 2;
|
const startAngle = -Math.PI / 2;
|
||||||
const endAngle = startAngle + (Math.PI * 2 * value / 100);
|
const endAngle = startAngle + (Math.PI * 2 * value / 100);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
||||||
ctx.strokeStyle = value > 90 ? '#dc3545' : value > 75 ? '#ffc107' : '#198754';
|
ctx.strokeStyle = value > 90 ? '#dc3545' : value > 75 ? '#ffc107' : '#198754';
|
||||||
@@ -543,7 +265,7 @@ function drawCpuGauge(value) {
|
|||||||
|
|
||||||
// Draw percentage text inside the circle
|
// Draw percentage text inside the circle
|
||||||
ctx.font = 'bold 20px Arial';
|
ctx.font = 'bold 20px Arial';
|
||||||
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bs-body-color');
|
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text');
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText(`${value.toFixed(1)}%`, centerX, centerY - 10);
|
ctx.fillText(`${value.toFixed(1)}%`, centerX, centerY - 10);
|
||||||
@@ -558,6 +280,10 @@ window.addEventListener('load', function() {
|
|||||||
const cpuValue = parseFloat(document.querySelector('.cpu-gauge canvas')?.getAttribute('data-value') || '0');
|
const cpuValue = parseFloat(document.querySelector('.cpu-gauge canvas')?.getAttribute('data-value') || '0');
|
||||||
drawCpuGauge(cpuValue);
|
drawCpuGauge(cpuValue);
|
||||||
});
|
});
|
||||||
|
new MutationObserver(() => {
|
||||||
|
const cpuValue = document.getElementById('cpuGauge')?.dataset.value;
|
||||||
|
if (cpuValue !== undefined) drawCpuGauge(cpuValue);
|
||||||
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+34
-49
@@ -5,6 +5,7 @@ ini_set('display_errors', 1);
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../includes/workspace.php';
|
||||||
|
|
||||||
// getDatabaseConnection relocated to central config
|
// getDatabaseConnection relocated to central config
|
||||||
|
|
||||||
@@ -98,11 +99,11 @@ if (isset($_GET['stream'])) {
|
|||||||
$matches = [];
|
$matches = [];
|
||||||
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
|
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
|
||||||
$start = intval($matches[1]);
|
$start = intval($matches[1]);
|
||||||
|
|
||||||
if (!empty($matches[2])) {
|
if (!empty($matches[2])) {
|
||||||
$end = intval($matches[2]);
|
$end = intval($matches[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$length = $end - $start + 1;
|
$length = $end - $start + 1;
|
||||||
|
|
||||||
header('HTTP/1.1 206 Partial Content');
|
header('HTTP/1.1 206 Partial Content');
|
||||||
@@ -135,13 +136,13 @@ if (isset($_GET['stream'])) {
|
|||||||
|
|
||||||
$readSize = min($chunkSize, $bytesRemaining);
|
$readSize = min($chunkSize, $bytesRemaining);
|
||||||
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
||||||
|
|
||||||
if ($chunkData !== false) {
|
if ($chunkData !== false) {
|
||||||
$bytesSent = strlen($chunkData);
|
$bytesSent = strlen($chunkData);
|
||||||
echo $chunkData;
|
echo $chunkData;
|
||||||
$bytesRemaining -= $bytesSent;
|
$bytesRemaining -= $bytesSent;
|
||||||
$currentPosition += $bytesSent;
|
$currentPosition += $bytesSent;
|
||||||
|
|
||||||
if (ob_get_level()) {
|
if (ob_get_level()) {
|
||||||
ob_flush();
|
ob_flush();
|
||||||
}
|
}
|
||||||
@@ -175,25 +176,25 @@ if (isset($_GET['download'])) {
|
|||||||
if (!$downloadAllowed) {
|
if (!$downloadAllowed) {
|
||||||
die("Download not permitted for this share.");
|
die("Download not permitted for this share.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download file code similar to download.php
|
// Download file code similar to download.php
|
||||||
header("Content-Type: application/octet-stream");
|
header("Content-Type: application/octet-stream");
|
||||||
header("Content-Disposition: attachment; filename=\"$fileName\"");
|
header("Content-Disposition: attachment; filename=\"$fileName\"");
|
||||||
header("Content-Length: $fileSize");
|
header("Content-Length: $fileSize");
|
||||||
|
|
||||||
$minChunkSize = 64 * 1024;
|
$minChunkSize = 64 * 1024;
|
||||||
$maxChunkSize = 2 * 1024 * 1024;
|
$maxChunkSize = 2 * 1024 * 1024;
|
||||||
$chunkSize = 256 * 1024;
|
$chunkSize = 256 * 1024;
|
||||||
|
|
||||||
$currentPosition = 0;
|
$currentPosition = 0;
|
||||||
$bytesRemaining = $fileSize;
|
$bytesRemaining = $fileSize;
|
||||||
|
|
||||||
set_time_limit(0);
|
set_time_limit(0);
|
||||||
|
|
||||||
while ($bytesRemaining > 0) {
|
while ($bytesRemaining > 0) {
|
||||||
$readSize = min($chunkSize, $bytesRemaining);
|
$readSize = min($chunkSize, $bytesRemaining);
|
||||||
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
||||||
|
|
||||||
if ($chunkData !== false) {
|
if ($chunkData !== false) {
|
||||||
$bytesRead = strlen($chunkData);
|
$bytesRead = strlen($chunkData);
|
||||||
echo $chunkData;
|
echo $chunkData;
|
||||||
@@ -218,35 +219,32 @@ function formatBytes($bytes, $precision = 2) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Shared Media - <?= htmlspecialchars($fileName) ?></title>
|
<title>Shared Media - <?= htmlspecialchars($fileName) ?></title>
|
||||||
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="stylesheet" href="../../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/view.css') ?>">
|
||||||
<link rel="stylesheet" href="css/view.css">
|
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
|
||||||
<script src="../../js/bootstrap.bundle.js"></script>
|
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-center">
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'shared',
|
||||||
<i class="bi"></i>
|
'section' => 'Shared file',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Shared with you',
|
||||||
</button>
|
'title' => $fileName,
|
||||||
</div>
|
'description' => 'Preview and access the file shared with you.',
|
||||||
<div class="custom-container dark-box">
|
'guest' => true,
|
||||||
<header class="row border-bottom m-5">
|
'class' => 'media-page',
|
||||||
<h1>Shared Media</h1>
|
]); ?>
|
||||||
<div class="mb-3">
|
|
||||||
<span class="badge bg-info">Shared Link</span>
|
|
||||||
<p class="text-muted small">This link will expire on <?= date('F j, Y, g:i a', strtotime($share['expiration'])) ?></p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="row">
|
|
||||||
|
<div class="content-toolbar"><span class="badge">Shared link</span><p>Available until <?= htmlspecialchars(date('F j, Y, g:i a', strtotime($share['expiration']))) ?></p></div>
|
||||||
|
<section class="content-panel media-panel">
|
||||||
<article class="col-12">
|
<article class="col-12">
|
||||||
<div class="media-container position-relative">
|
<div class="media-container position-relative">
|
||||||
<?php if ($isImage): ?>
|
<?php if ($isImage): ?>
|
||||||
@@ -293,33 +291,20 @@ function formatBytes($bytes, $precision = 2) {
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
|
<?php renderWorkspaceEnd(); ?>
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../../js/theme.js"></script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mediaElement = document.querySelector('video, audio');
|
const mediaElement = document.querySelector('video, audio');
|
||||||
|
|
||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
const savedVolume = localStorage.getItem('mediaVolume');
|
const savedVolume = localStorage.getItem('mediaVolume');
|
||||||
if (savedVolume !== null) {
|
if (savedVolume !== null) {
|
||||||
mediaElement.volume = parseFloat(savedVolume);
|
mediaElement.volume = parseFloat(savedVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaElement.addEventListener('volumechange', () => {
|
mediaElement.addEventListener('volumechange', () => {
|
||||||
localStorage.setItem('mediaVolume', mediaElement.volume);
|
localStorage.setItem('mediaVolume', mediaElement.volume);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function createDirectoryRecursive($sftp, $path) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parent = dirname($path);
|
$parent = remoteDirname($path);
|
||||||
if ($parent != '/' && !$sftp->is_dir($parent)) {
|
if ($parent != '/' && !$sftp->is_dir($parent)) {
|
||||||
createDirectoryRecursive($sftp, $parent);
|
createDirectoryRecursive($sftp, $parent);
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (!empty($relativePath)) {
|
if (!empty($relativePath)) {
|
||||||
$fileName = basename($relativePath);
|
$fileName = basename($relativePath);
|
||||||
|
|
||||||
$dirPart = dirname($relativePath);
|
$dirPart = remoteDirname($relativePath);
|
||||||
if ($dirPart !== '.' && $dirPart !== '') {
|
if ($dirPart !== '.' && $dirPart !== '') {
|
||||||
$remoteDirPath = $currentPath . '/' . $dirPart;
|
$remoteDirPath = $currentPath . '/' . $dirPart;
|
||||||
|
|
||||||
|
|||||||
+48
-62
@@ -14,6 +14,7 @@ error_reporting(E_ALL);
|
|||||||
ini_set('memory_limit', '512M');
|
ini_set('memory_limit', '512M');
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../includes/workspace.php';
|
||||||
$sftp = initializeSFTP();
|
$sftp = initializeSFTP();
|
||||||
|
|
||||||
if (!isset($_GET['file'])) {
|
if (!isset($_GET['file'])) {
|
||||||
@@ -34,10 +35,10 @@ if (isset($_POST['createShare'])) {
|
|||||||
$conn = getDatabaseConnection();
|
$conn = getDatabaseConnection();
|
||||||
$stmt = $conn->prepare("INSERT INTO share (token, file_path, expiration, download_allowed) VALUES (?, ?, ?, ?)");
|
$stmt = $conn->prepare("INSERT INTO share (token, file_path, expiration, download_allowed) VALUES (?, ?, ?, ?)");
|
||||||
$stmt->bind_param("sssi", $token, $filePath, $expiration, $downloadAllowed);
|
$stmt->bind_param("sssi", $token, $filePath, $expiration, $downloadAllowed);
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
if ($stmt->execute()) {
|
||||||
$shareUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" .
|
$shareUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" .
|
||||||
dirname($_SERVER['PHP_SELF']) . "/shared.php?token=$token";
|
remoteDirname($_SERVER['PHP_SELF']) . "/shared.php?token=$token";
|
||||||
|
|
||||||
$_SESSION['shareCreated'] = true;
|
$_SESSION['shareCreated'] = true;
|
||||||
$_SESSION['shareUrl'] = $shareUrl;
|
$_SESSION['shareUrl'] = $shareUrl;
|
||||||
@@ -50,7 +51,7 @@ if (isset($_POST['createShare'])) {
|
|||||||
header("Location: view.php?file=" . urlencode($filePath) . "&shared=0");
|
header("Location: view.php?file=" . urlencode($filePath) . "&shared=0");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
$conn->close();
|
$conn->close();
|
||||||
}
|
}
|
||||||
@@ -142,11 +143,11 @@ if (isset($_GET['stream'])) {
|
|||||||
$matches = [];
|
$matches = [];
|
||||||
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
|
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
|
||||||
$start = intval($matches[1]);
|
$start = intval($matches[1]);
|
||||||
|
|
||||||
if (!empty($matches[2])) {
|
if (!empty($matches[2])) {
|
||||||
$end = intval($matches[2]);
|
$end = intval($matches[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$length = $end - $start + 1;
|
$length = $end - $start + 1;
|
||||||
|
|
||||||
header('HTTP/1.1 206 Partial Content');
|
header('HTTP/1.1 206 Partial Content');
|
||||||
@@ -193,18 +194,18 @@ if (isset($_GET['stream'])) {
|
|||||||
|
|
||||||
// Calculate adaptive chunk size
|
// Calculate adaptive chunk size
|
||||||
$readSize = min($chunkSize, $bytesRemaining);
|
$readSize = min($chunkSize, $bytesRemaining);
|
||||||
|
|
||||||
// Get chunk from SFTP
|
// Get chunk from SFTP
|
||||||
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
$chunkData = $sftp->get($filePath, false, $currentPosition, $readSize);
|
||||||
|
|
||||||
if ($chunkData !== false) {
|
if ($chunkData !== false) {
|
||||||
$bytesSent = strlen($chunkData);
|
$bytesSent = strlen($chunkData);
|
||||||
|
|
||||||
// Output chunk
|
// Output chunk
|
||||||
echo $chunkData;
|
echo $chunkData;
|
||||||
$bytesRemaining -= $bytesSent;
|
$bytesRemaining -= $bytesSent;
|
||||||
$currentPosition += $bytesSent;
|
$currentPosition += $bytesSent;
|
||||||
|
|
||||||
// Flush buffers
|
// Flush buffers
|
||||||
if (ob_get_level()) {
|
if (ob_get_level()) {
|
||||||
ob_flush();
|
ob_flush();
|
||||||
@@ -253,34 +254,32 @@ if (isset($_GET['stream'])) {
|
|||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Media Viewer - <?= htmlspecialchars($fileName) ?></title>
|
<title>Media Viewer - <?= htmlspecialchars($fileName) ?></title>
|
||||||
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="stylesheet" href="../../css/bootstrap.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
|
||||||
<link rel="stylesheet" href="../../css/modern.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/view.css') ?>">
|
||||||
<link rel="stylesheet" href="css/view.css">
|
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
|
||||||
<script src="../../js/bootstrap.bundle.js"></script>
|
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-center">
|
<?php renderWorkspaceStart([
|
||||||
<div class="d-flex justify-content-end p-3">
|
'root' => '../../',
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
'active' => 'files',
|
||||||
<i class="bi"></i>
|
'section' => 'Media preview',
|
||||||
<span id="themeText"></span>
|
'eyebrow' => 'Media preview',
|
||||||
</button>
|
'title' => $fileName,
|
||||||
</div>
|
'description' => 'Preview your media, download a copy or create a shared link.',
|
||||||
<div class="custom-container dark-box">
|
'connected' => true,
|
||||||
<header class="row border-bottom m-5">
|
'back' => 'index.php?path=' . urlencode(remoteDirname($filePath)),
|
||||||
<h1>Media Viewer</h1>
|
'class' => 'media-page',
|
||||||
<div class="mb-3 p-3">
|
]); ?>
|
||||||
<a href="index.php?path=<?= urlencode(dirname($filePath)) ?>" class="btn btn-primary">Back to Files</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="row">
|
|
||||||
|
<section class="content-panel media-panel">
|
||||||
<article class="col-12">
|
<article class="col-12">
|
||||||
<div class="media-container position-relative">
|
<div class="media-container position-relative">
|
||||||
<?php if ($isImage): ?>
|
<?php if ($isImage): ?>
|
||||||
@@ -305,13 +304,13 @@ if (isset($_GET['stream'])) {
|
|||||||
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
|
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
|
||||||
<a href="download.php?file=<?= urlencode($filePath) ?>" class="btn btn-success m-3">Download</a>
|
<a href="download.php?file=<?= urlencode($filePath) ?>" class="btn btn-success m-3">Download</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Add Share Button -->
|
<!-- Add Share Button -->
|
||||||
<button type="button" class="btn btn-info m-3" data-bs-toggle="modal" data-bs-target="#shareModal">
|
<button type="button" class="btn btn-info m-3" data-dialog="shareModal">
|
||||||
<i class="bi bi-share"></i> Share
|
<i class="ui-icon icon-share"></i> Share
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share success message -->
|
<!-- Share success message -->
|
||||||
<?php if ($shareCreated): ?>
|
<?php if ($shareCreated): ?>
|
||||||
<div class="alert alert-success mt-3">
|
<div class="alert alert-success mt-3">
|
||||||
@@ -319,7 +318,7 @@ if (isset($_GET['stream'])) {
|
|||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control" id="shareUrl" value="<?= htmlspecialchars($shareUrl) ?>" readonly>
|
<input type="text" class="form-control" id="shareUrl" value="<?= htmlspecialchars($shareUrl) ?>" readonly>
|
||||||
<button class="btn btn-outline-secondary" type="button" onclick="copyShareUrl()">
|
<button class="btn btn-outline-secondary" type="button" onclick="copyShareUrl()">
|
||||||
<i class="bi bi-clipboard"></i> Copy
|
<i class="ui-icon icon-clipboard"></i> Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted">This link will expire on <?= date('F j, Y, g:i a', strtotime($shareExpiration)) ?></p>
|
<p class="small text-muted">This link will expire on <?= date('F j, Y, g:i a', strtotime($shareExpiration)) ?></p>
|
||||||
@@ -356,33 +355,20 @@ if (isset($_GET['stream'])) {
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
|
<?php renderWorkspaceEnd(); ?>
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Share Modal -->
|
<!-- Share Modal -->
|
||||||
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
|
<dialog class="modal" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="shareModalLabel">Share "<?= htmlspecialchars($fileName) ?>"</h5>
|
<h5 class="modal-title" id="shareModalLabel">Share "<?= htmlspecialchars($fileName) ?>"</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<input type="hidden" name="filePath" value="<?= htmlspecialchars($filePath) ?>">
|
<input type="hidden" name="filePath" value="<?= htmlspecialchars($filePath) ?>">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="duration" class="form-label">Link expires after</label>
|
<label for="duration" class="form-label">Link expires after</label>
|
||||||
<select class="form-select" id="duration" name="duration">
|
<select class="form-select" id="duration" name="duration">
|
||||||
@@ -393,45 +379,45 @@ if (isset($_GET['stream'])) {
|
|||||||
<option value="720">30 days</option>
|
<option value="720">30 days</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="downloadAllowed" name="downloadAllowed">
|
<input type="checkbox" class="form-check-input" id="downloadAllowed" name="downloadAllowed">
|
||||||
<label class="form-check-label" for="downloadAllowed">Allow downloading</label>
|
<label class="form-check-label" for="downloadAllowed">Allow downloading</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
|
||||||
<button type="submit" name="createShare" class="btn btn-primary">Create Share Link</button>
|
<button type="submit" name="createShare" class="btn btn-primary">Create Share Link</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function copyShareUrl() {
|
function copyShareUrl() {
|
||||||
const shareUrl = document.getElementById('shareUrl');
|
const shareUrl = document.getElementById('shareUrl');
|
||||||
shareUrl.select();
|
shareUrl.select();
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
|
|
||||||
// Show feedback
|
// Show feedback
|
||||||
const button = shareUrl.nextElementSibling;
|
const button = shareUrl.nextElementSibling;
|
||||||
const originalText = button.innerHTML;
|
const originalText = button.innerHTML;
|
||||||
button.innerHTML = '<i class="bi bi-check"></i> Copied!';
|
button.innerHTML = '<i class="ui-icon icon-check"></i> Copied!';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.innerHTML = originalText;
|
button.innerHTML = originalText;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../../js/theme.js"></script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mediaElement = document.querySelector('video, audio');
|
const mediaElement = document.querySelector('video, audio');
|
||||||
|
|
||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
// Restore volume from localStorage
|
// Restore volume from localStorage
|
||||||
const savedVolume = localStorage.getItem('mediaVolume');
|
const savedVolume = localStorage.getItem('mediaVolume');
|
||||||
|
|||||||
+11
-47
@@ -1,47 +1,11 @@
|
|||||||
html, body {
|
.login-page { display:flex; min-height:100dvh; flex-direction:column; }
|
||||||
height: 100%;
|
.login-top,.login-footer { display:flex; align-items:center; justify-content:space-between; padding:28px 5vw; gap:16px; }
|
||||||
margin: 0;
|
.login-brand { display:flex; align-items:center; gap:12px; font-size:19px; color:var(--text); font-weight:750; letter-spacing:-.03em; } .login-brand > span { display:grid; place-items:center; background:#2563eb; color:white; width:36px; height:36px; border-radius:9px; font-size:25px; }
|
||||||
}
|
.login-layout { flex:1; width:min(1060px,90%); margin:48px auto; display:grid; grid-template-columns:1.1fr 1fr; align-items:center; gap:100px; }
|
||||||
|
.login-story h1 { font-size:clamp(44px,5vw,64px); margin:16px 0 20px; line-height:1.08; letter-spacing:-.055em; } .login-story h1 > span { color:var(--blue); } .login-story > p { max-width:370px; color:var(--muted); font-size:15px; line-height:1.8; }
|
||||||
body {
|
.login-card { background:var(--surface); padding:40px; border:1px solid var(--line); border-radius:18px; box-shadow:var(--shadow); } .login-card h2 { margin:12px 0 8px; font-size:24px; } .login-card form { margin-top:30px; } .login-card label { font-size:12px; } .login-card .btn { width:100%; justify-content:space-between; padding:13px 16px; } .login-note { margin-top:28px; padding-top:22px; border-top:1px solid var(--line); font-size:11px; color:var(--muted); text-align:center; }
|
||||||
display: flex;
|
.storage-illustration { max-width:310px; margin-top:35px; padding:18px; border:1px solid var(--line); border-radius:14px; background:var(--surface); transform:rotate(-3deg); box-shadow:12px 14px 0 var(--blue-soft); }
|
||||||
flex-direction: column;
|
.storage-top { display:flex; justify-content:space-between; font-size:10px; font-weight:700; letter-spacing:.12em; margin-bottom:14px; } .storage-top span { color:var(--blue); } .storage-drive { display:flex; align-items:center; gap:16px; height:38px; padding:0 12px; border:1px solid var(--line); border-radius:6px; margin:6px 0; background:var(--bg); } .storage-drive b { font:10px ui-monospace,monospace; color:var(--muted); } .storage-drive > span { flex:1; height:5px; border-radius:4px; background:var(--line); } .storage-drive i { width:5px; height:5px; border-radius:50%; background:var(--blue); } .storage-bottom { color:var(--muted); font-size:7px; letter-spacing:.14em; margin-top:14px; }
|
||||||
}
|
.login-footer { font-size:11px; } .login-footer a { color:var(--muted); }
|
||||||
|
@media(max-width:800px) { .login-layout { grid-template-columns:1fr 1fr; gap:30px; } .login-card { padding:26px; } }
|
||||||
main {
|
@media(max-width:620px) { .login-layout { display:block; max-width:430px; margin:24px auto; } .login-story { margin-bottom:30px; } .login-story h1 { font-size:38px; } .login-story h1 br { display:none; } .storage-illustration { display:none; } .login-story > p { font-size:13px; } .login-top,.login-footer { padding:20px; } .login-footer { flex-wrap:wrap; } }
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixed {
|
|
||||||
-ms-flex: 0 0 300px;
|
|
||||||
flex: 0 0 300px;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-effect:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light .dark-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dark .light-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
+71
-181
@@ -1,181 +1,71 @@
|
|||||||
/* Modern dark theme with blue accents */
|
/* Shared UI primitives, independent of a CSS framework. */
|
||||||
:root {
|
:root { color-scheme: light; --bg:#f5f7fb; --surface:#fff; --surface-alt:#f0f4fa; --text:#17243b; --muted:#64748b; --line:#e0e7f0; --blue:#2563eb; --blue-soft:#eaf1ff; --danger:#c83b50; --success:#168364; --shadow:0 16px 48px #16294a09; --body-bg-rgb:245,247,251; }
|
||||||
--bg-dark: #171b22;
|
:root[data-theme="dark"] { color-scheme:dark; --bg:#0c1220; --surface:#121c2d; --surface-alt:#19253a; --text:#e4ecfa; --muted:#96a6bf; --line:#26334a; --blue:#79a7ff; --blue-soft:#1c3053; --danger:#ff8797; --success:#64d3ae; --shadow:0 16px 48px #0002; --body-bg-rgb:12,18,32; }
|
||||||
--bg-dark2: #1a202c;
|
* { box-sizing:border-box; }
|
||||||
--primary-blue: #3b82f6;
|
body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.55 'Segoe UI',system-ui,sans-serif; }
|
||||||
--text-light: #e5e7eb;
|
button,input,select,textarea { font:inherit; }
|
||||||
--bg-darkblue: #101722;
|
a { color:var(--blue); text-decoration:none; } a:hover { text-decoration:underline; }
|
||||||
--container-bg-start: #1a2029;
|
:focus-visible { outline:3px solid var(--blue); outline-offset:3px; }
|
||||||
--container-bg-end: #151d29;
|
[hidden] { display:none !important; }
|
||||||
--sftp-bg-start: #151b25;
|
h1,h2,h3,h4,h5,p { margin:0 0 1rem; }
|
||||||
--sftp-bg-end: #101722;
|
h1 { font-size:clamp(26px,3vw,36px); letter-spacing:-.045em; line-height:1.2; }
|
||||||
--container-border: rgba(77, 145, 255, 0.7);
|
h2 { font-size:23px; letter-spacing:-.03em; } h3 { font-size:18px; } h4,h5 { font-size:16px; }
|
||||||
--panel-text: #e5e7eb;
|
small,.small,.form-text { font-size:12px; }
|
||||||
--panel-muted: #aab4c3;
|
.text-muted,.form-text { color:var(--muted); } .text-danger { color:var(--danger); } .text-success { color:var(--success); }
|
||||||
--control-bg: #101722;
|
.text-center { text-align:center; } .text-end { text-align:right; } .text-break,.text-wrap { overflow-wrap:anywhere; } .fw-bold { font-weight:650; }
|
||||||
--control-border: #39485d;
|
.btn { display:inline-flex; justify-content:center; align-items:center; gap:8px; min-height:38px; padding:8px 14px; border:1px solid var(--line); border-radius:8px; background:var(--surface); color:var(--text); cursor:pointer; font-weight:600; line-height:1.4; text-decoration:none; transition:background .15s,border-color .15s; }
|
||||||
}
|
.btn:hover { background:var(--surface-alt); border-color:var(--muted); text-decoration:none; }
|
||||||
|
.btn-primary { background:#2563eb; border-color:#2563eb; color:#fff; } .btn-primary:hover { background:#1d4ed8; border-color:#1d4ed8; }
|
||||||
body {
|
.btn-danger { color:var(--danger); } .btn-success,.btn-info { color:var(--blue); }
|
||||||
background: linear-gradient(135deg, var(--bg-dark), var(--bg-darkblue));
|
.btn-sm { min-height:32px; padding:6px 10px; font-size:12px; } .btn:disabled,.btn.disabled { opacity:.5; cursor:default; }
|
||||||
color: var(--text-light);
|
.btn-group,.input-group { display:flex; gap:8px; }
|
||||||
min-height: 100vh;
|
.form-control,.form-select { display:block; width:100%; min-width:0; border:1px solid var(--line); border-radius:8px; background:var(--surface); color:var(--text); padding:11px 13px; }
|
||||||
font-family: system-ui, sans-serif;
|
.form-control::placeholder { color:var(--muted); } .form-control:focus,.form-select:focus { border-color:var(--blue); outline:3px solid var(--blue-soft); }
|
||||||
margin: 0;
|
label,.form-label { display:inline-block; margin-bottom:6px; font-weight:600; } input[type="checkbox"] { accent-color:var(--blue); width:16px; height:16px; }
|
||||||
padding: 0;
|
.form-check { display:flex; align-items:center; gap:8px; } .form-check label { margin:0; }
|
||||||
}
|
.surface-panel,.card { background:var(--surface); border:1px solid var(--line); border-radius:12px; padding:24px; }
|
||||||
|
.card-header { border-bottom:1px solid var(--line); padding:12px; } .card-body { padding:20px; }
|
||||||
a, .nav-link {
|
.custom-container.dark-box { width:calc(100% - 48px); max-width:1280px; min-height:calc(100vh - 100px); margin:0 auto 24px; padding:24px; background:var(--surface); border:1px solid var(--line); border-radius:16px; box-shadow:var(--shadow); }
|
||||||
color: var(--primary-blue) !important;
|
.custom-container > header { padding-bottom:20px; margin:8px 0 24px; }
|
||||||
transition: opacity .2s ease-in-out;
|
table { width:100%; border-collapse:collapse; text-align:left; } th { color:var(--muted); font-size:11px; font-weight:650; letter-spacing:.06em; text-transform:uppercase; background:var(--surface-alt); }
|
||||||
}
|
th,td { padding:14px 16px; border-bottom:1px solid var(--line); overflow-wrap:anywhere; } tbody tr:hover { background:var(--surface-alt); }
|
||||||
|
.alert { padding:14px 16px; margin:16px 0; border:1px solid var(--line); border-radius:8px; background:var(--blue-soft); text-align:left; } .alert-danger { color:var(--danger); } .alert-success { color:var(--success); }
|
||||||
body, button, input, select, textarea {
|
.badge { display:inline-block; padding:2px 7px; border-radius:5px; background:var(--surface-alt); color:var(--muted); font-size:10px; font-weight:600; }
|
||||||
font-family: system-ui, sans-serif;
|
.progress { height:8px; background:var(--surface-alt); border-radius:8px; overflow:hidden; } .progress-bar { height:100%; background:var(--blue); }
|
||||||
}
|
.bg-success { background:var(--success); } .bg-warning { background:#d99b32; } .bg-danger { background:var(--danger); }
|
||||||
|
.badge.bg-danger,.badge.bg-success,.badge.bg-warning { background:var(--surface-alt); color:var(--muted); }
|
||||||
/* Keep text, fields and tables readable on every page that uses the shared theme. */
|
.img-fluid { max-width:100%; height:auto; }
|
||||||
.surface-panel,
|
html:not([data-theme="dark"]) .dark-logo,[data-theme="dark"] .light-logo { display:none; }
|
||||||
.dark-box {
|
footer { color:var(--muted); font-size:12px; } footer img { opacity:.7; max-height:22px; object-fit:contain; }
|
||||||
color: var(--panel-text);
|
.theme-toggle { white-space:nowrap; }
|
||||||
}
|
.d-flex { display:flex; } .flex-column { flex-direction:column; } .flex-wrap { flex-wrap:wrap; } .justify-content-end { justify-content:flex-end; } .justify-content-center { justify-content:center; } .justify-content-between { justify-content:space-between; } .align-items-center { align-items:center; } .gap-3 { gap:16px; }
|
||||||
|
.row { display:flex; flex-wrap:wrap; gap:20px; } .col { flex:1; min-width:0; } .col-12 { width:100%; } .col-4 { flex:1; }
|
||||||
.surface-panel .text-muted,
|
.container,.container-fluid { width:100%; margin-inline:auto; } .container { max-width:1280px; }
|
||||||
.dark-box .text-muted {
|
.border { border:1px solid var(--line); } .border-top { border-top:1px solid var(--line); } .border-bottom { border-bottom:1px solid var(--line); } .rounded { border-radius:10px; }
|
||||||
color: var(--panel-muted) !important;
|
.position-relative { position:relative; } .w-100 { width:100%; } .h-100 { height:100%; } .w-auto { width:auto; } .mx-auto { margin-inline:auto; }
|
||||||
}
|
.p-2 { padding:12px; } .p-3 { padding:20px; } .m-3 { margin:20px; } .m-5 { margin:32px; }
|
||||||
|
.mb-1,.mt-1 { margin-block:4px; } .mb-2 { margin-bottom:8px; } .mb-3 { margin-bottom:16px; } .mb-4 { margin-bottom:24px; } .mt-2 { margin-top:8px; } .mt-3 { margin-top:16px; } .mt-4 { margin-top:24px; } .me-2 { margin-right:8px; }
|
||||||
.surface-panel .form-control,
|
dialog.modal { width:min(480px,calc(100% - 32px)); padding:0; border:1px solid var(--line); border-radius:16px; background:var(--surface); color:var(--text); box-shadow:0 24px 100px #0005; max-height:calc(100dvh - 40px); }
|
||||||
.surface-panel .form-select,
|
dialog::backdrop { background:#080f2199; backdrop-filter:blur(4px); }
|
||||||
.dark-box .form-control,
|
.modal-header,.modal-footer { padding:20px 24px; display:flex; align-items:center; justify-content:space-between; gap:12px; } .modal-header { border-bottom:1px solid var(--line); } .modal-title { margin:0; } .modal-body { padding:24px; } .modal-footer { justify-content:flex-end; border-top:1px solid var(--line); }
|
||||||
.dark-box .form-select {
|
.btn-close { border:0; background:transparent; color:var(--muted); cursor:pointer; font-size:24px; } .btn-close::before { content:'×'; } body:has(dialog[open]) { overflow:hidden; }
|
||||||
color: var(--panel-text);
|
.eyebrow { color:var(--muted); font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; }
|
||||||
background-color: var(--control-bg);
|
@media(min-width:900px) { .col-md-6 { width:calc(50% - 10px); } .col-lg-4 { width:calc(33.333% - 14px); } }
|
||||||
border-color: var(--control-border);
|
@media(max-width:700px) { .custom-container.dark-box { width:calc(100% - 24px); padding:16px; } .surface-panel { overflow-x:auto; } .row { gap:12px; } .col-md-6,.col-lg-4 { width:100%; } th,td { padding:10px; } .m-5 { margin:16px; } }
|
||||||
}
|
@media(prefers-reduced-motion:reduce) { *,*::before,*::after { transition:none !important; animation:none !important; } }
|
||||||
|
/* Small local icon vocabulary used by secondary screens. */
|
||||||
.surface-panel .form-control::placeholder,
|
.ui-icon { font-style:normal; display:inline-block; }
|
||||||
.dark-box .form-control::placeholder {
|
.icon-chevron-left::before { content:'‹'; } .icon-chevron-right::before { content:'›'; }
|
||||||
color: var(--panel-muted);
|
.icon-fullscreen::before { content:'⛶'; } .icon-fullscreen-exit::before { content:'⊡'; }
|
||||||
}
|
.icon-share::before { content:'↗'; } .icon-clipboard::before { content:'▤'; }
|
||||||
|
.icon-check::before,.icon-check-circle-fill::before { content:'✓'; }
|
||||||
.surface-panel .form-control:focus,
|
.icon-x-circle-fill::before { content:'×'; } .icon-exclamation-triangle::before { content:'!'; }
|
||||||
.surface-panel .form-select:focus,
|
.icon-server::before,.icon-hdd::before,.icon-hdd-stack::before { content:'▤'; }
|
||||||
.dark-box .form-control:focus,
|
.icon-clock::before { content:'◷'; } .icon-cpu::before { content:'▦'; } .icon-memory::before { content:'▥'; }
|
||||||
.dark-box .form-select:focus {
|
.text-primary { color:var(--blue); } .text-warning { color:#c98d22; }
|
||||||
border-color: var(--primary-blue);
|
.spinner-border { display:inline-block; width:14px; height:14px; border:2px solid var(--line); border-top-color:var(--blue); border-radius:50%; animation:spin .8s linear infinite; }
|
||||||
box-shadow: 0 0 0 .2rem rgba(59, 130, 246, .2);
|
@keyframes spin { to { transform:rotate(360deg); } }
|
||||||
}
|
/* Secondary screens share the same surfaces and spacing. */
|
||||||
|
.col-lg-10 { width:100%; } .py-4 { padding-block:24px; } .mb-0 { margin-bottom:0; } .h4 { font-size:22px; } .h5 { font-size:16px; }
|
||||||
.surface-panel .table,
|
.card-header.bg-success,.card-header.bg-warning,.card-header.bg-danger,.card-header.bg-primary,.card-header.bg-info { background:var(--surface-alt); }
|
||||||
.surface-panel .table th,
|
.custom-container.dark-box.fullscreen-mode { max-width:none; width:100%; height:100dvh; margin:0; border:0; border-radius:0; padding:0; }
|
||||||
.surface-panel .table td {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
a:hover, .btn:hover, .nav-link:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure action buttons have white text for visibility */
|
|
||||||
.btn-primary,
|
|
||||||
.btn-danger,
|
|
||||||
.btn-warning,
|
|
||||||
.btn-success,
|
|
||||||
.btn-info,
|
|
||||||
.btn-secondary {
|
|
||||||
color: var(--text-light) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override specific actions (update password, upload directory, rename) to have black text */
|
|
||||||
.btn-primary,
|
|
||||||
.btn-warning,
|
|
||||||
.btn-info {
|
|
||||||
color: #212529 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-box {
|
|
||||||
background: rgba(0,0,0,0.6);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 1rem;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-panel {
|
|
||||||
background: linear-gradient(145deg, var(--sftp-bg-start), var(--sftp-bg-end));
|
|
||||||
color: var(--text-light);
|
|
||||||
border: 1px solid rgba(77, 145, 255, 0.3);
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow: 0 12px 32px rgba(3, 8, 18, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: rgba(20, 20, 30, 0.9) !important;
|
|
||||||
color: var(--text-light) !important;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
color: var(--text-light) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th, .table td {
|
|
||||||
border-color: rgba(255,255,255,0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main window styling (window within window) */
|
|
||||||
.custom-container.dark-box {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 2rem auto;
|
|
||||||
background: linear-gradient(135deg, var(--container-bg-start), var(--container-bg-end));
|
|
||||||
border: 2px solid var(--container-border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 1rem;
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light mode overrides */
|
|
||||||
html[data-bs-theme="light"] {
|
|
||||||
--bg-light: #f8f9fa;
|
|
||||||
--bg-light2: #ffffff;
|
|
||||||
--container-bg: rgba(255,255,255,0.85);
|
|
||||||
--container-border: var(--primary-blue);
|
|
||||||
--text-dark: #212529;
|
|
||||||
--panel-text: #212529;
|
|
||||||
--panel-muted: #596579;
|
|
||||||
--control-bg: #ffffff;
|
|
||||||
--control-border: #c8d2df;
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] body {
|
|
||||||
background: var(--bg-light);
|
|
||||||
color: var(--text-dark);
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] .dark-box {
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
color: var(--text-dark);
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] .surface-panel .form-control,
|
|
||||||
html[data-bs-theme="light"] .surface-panel .form-select,
|
|
||||||
html[data-bs-theme="light"] .dark-box .form-control,
|
|
||||||
html[data-bs-theme="light"] .dark-box .form-select {
|
|
||||||
color: var(--panel-text);
|
|
||||||
background-color: var(--control-bg);
|
|
||||||
border-color: var(--control-border);
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] .form-control::placeholder {
|
|
||||||
color: var(--panel-muted);
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] .surface-panel {
|
|
||||||
background: linear-gradient(145deg, #ffffff, #edf2f8);
|
|
||||||
color: var(--text-dark);
|
|
||||||
border-color: rgba(59, 130, 246, 0.3);
|
|
||||||
box-shadow: 0 12px 32px rgba(31, 58, 92, 0.08);
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] .modal-content {
|
|
||||||
background: #fff !important;
|
|
||||||
color: var(--text-dark) !important;
|
|
||||||
}
|
|
||||||
html[data-bs-theme="light"] .table {
|
|
||||||
color: var(--text-dark) !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* Persistent workspace frame shared by every signed-in page and shared previews. */
|
||||||
|
.file-app { display:grid; grid-template-columns:236px minmax(0,1fr); text-align:left; }
|
||||||
|
.skip-link { position:fixed; left:250px; top:-60px; z-index:10000; padding:10px 16px; background:var(--surface); border:1px solid var(--blue); border-radius:8px; }
|
||||||
|
.skip-link:focus { top:12px; }
|
||||||
|
.sidebar { position:sticky; top:0; height:100dvh; padding:30px 20px 16px; display:flex; flex-direction:column; background:var(--surface); border-right:1px solid var(--line); overflow-y:auto; }
|
||||||
|
.brand { display:flex; align-items:center; gap:12px; color:var(--text); font-size:19px; font-weight:750; letter-spacing:-.04em; text-decoration:none; }
|
||||||
|
.brand-mark { display:grid; place-items:center; width:40px; height:40px; border-radius:11px; background:#2563eb; color:white; font-size:27px; }
|
||||||
|
.brand-caption { display:block; font-size:9px; font-weight:600; letter-spacing:.16em; color:var(--muted); margin-top:1px; }
|
||||||
|
.nav-heading { margin:48px 12px 12px; }
|
||||||
|
.nav-item { display:flex; align-items:center; gap:12px; padding:11px 12px; margin:4px 0; color:var(--muted); border-radius:8px; font-weight:550; }
|
||||||
|
.nav-item:hover { text-decoration:none; background:var(--surface-alt); color:var(--text); }
|
||||||
|
.nav-item.active { background:var(--blue-soft); color:var(--blue); }
|
||||||
|
.nav-item > span { font-size:20px; width:21px; text-align:center; }
|
||||||
|
.sidebar-bottom { margin-top:auto; padding-top:40px; }
|
||||||
|
.connection-card { border:1px solid var(--line); border-radius:10px; padding:14px 12px; font-size:12px; font-weight:600; margin-bottom:18px; }
|
||||||
|
.status-dot { display:inline-block; width:6px; height:6px; background:var(--success); border-radius:50%; margin-right:7px; }
|
||||||
|
.connection-card small { display:block; margin-top:5px; color:var(--muted); font-weight:400; font-size:10px; }
|
||||||
|
.user-card { display:flex; gap:10px; align-items:center; padding:16px 4px 0; margin-top:14px; border-top:1px solid var(--line); }
|
||||||
|
.avatar { display:grid; place-items:center; background:var(--blue-soft); color:var(--blue); width:34px; height:34px; border-radius:50%; font-weight:700; flex-shrink:0; }
|
||||||
|
.user-name { flex:1; overflow-wrap:anywhere; font-weight:600; font-size:12px; }
|
||||||
|
.user-name small { display:block; font-size:10px; font-weight:400; color:var(--muted); }
|
||||||
|
.workspace { min-width:0; display:flex; flex-direction:column; min-height:100dvh; }
|
||||||
|
.topbar { min-height:76px; display:flex; justify-content:space-between; align-items:center; gap:12px; padding:16px 40px; border-bottom:1px solid var(--line); background:var(--surface); color:var(--muted); font-size:12px; }
|
||||||
|
.topbar strong { color:var(--text); font-weight:500; }
|
||||||
|
.topbar-divider { padding:0 14px; opacity:.5; }
|
||||||
|
.file-main { flex:1; width:100%; max-width:1560px; margin:0 auto; padding:42px 40px 0; display:flex; flex-direction:column; min-width:0; }
|
||||||
|
.workspace-content { flex:1; min-width:0; }
|
||||||
|
.page-heading { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-bottom:30px; }
|
||||||
|
.page-heading .eyebrow { margin-bottom:9px; }
|
||||||
|
.page-heading h1 { margin-bottom:10px; font-size:36px; overflow-wrap:anywhere; }
|
||||||
|
.heading-dot { color:var(--blue); }
|
||||||
|
.page-heading p { color:var(--muted); margin:0; font-size:13px; }
|
||||||
|
.heading-back { white-space:nowrap; font-size:12px; flex-shrink:0; }
|
||||||
|
.protocol-badge { border:1px solid var(--line); border-radius:6px; padding:5px 10px; font:11px ui-monospace,monospace; background:var(--surface); white-space:nowrap; }
|
||||||
|
.workspace-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:22px 0; font-size:10px; margin:0; border:0; }
|
||||||
|
.workspace-footer a { color:var(--muted); }
|
||||||
|
.content-panel { padding:24px; border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); min-width:0; }
|
||||||
|
.panel-heading { margin-bottom:22px; } .panel-heading h2 { margin-bottom:6px; font-size:18px; } .panel-heading p { margin:0; color:var(--muted); font-size:12px; }
|
||||||
|
.content-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:20px; font-size:12px; color:var(--muted); }
|
||||||
|
.content-toolbar p { margin:0; }
|
||||||
|
.table-scroll { overflow-x:auto; max-width:100%; }
|
||||||
|
@media(min-width:1500px) { .file-main { padding-top:56px; } }
|
||||||
|
@media(max-width:1100px) { .file-app { grid-template-columns:200px minmax(0,1fr); } .sidebar { padding-inline:14px; } .nav-item { font-size:13px; } .file-main { padding:28px 24px 0; } .topbar { padding-inline:24px; } }
|
||||||
|
@media(max-width:760px) {
|
||||||
|
.file-app { display:block; }
|
||||||
|
.sidebar { position:static; height:auto; padding:16px; border-right:0; border-bottom:1px solid var(--line); overflow:visible; }
|
||||||
|
.brand-caption,.nav-heading,.connection-card { display:none; }
|
||||||
|
.sidebar nav { display:flex; flex-wrap:wrap; gap:6px; margin-top:14px; }
|
||||||
|
.nav-item { font-size:12px; padding:7px 10px; margin:0; }
|
||||||
|
.sidebar-bottom { margin:8px 0 0; padding:0; display:flex; }
|
||||||
|
.sidebar-bottom .nav-item { padding:4px 10px; }
|
||||||
|
.sidebar-bottom .user-card { display:flex; margin:0 0 0 auto; padding:0 10px; border:0; }
|
||||||
|
.sidebar-bottom .user-name,.sidebar-bottom .avatar { display:none; }
|
||||||
|
.topbar { min-height:58px; padding:12px 16px; }
|
||||||
|
.file-main { padding:26px 16px 0; }
|
||||||
|
.page-heading { align-items:flex-start; flex-wrap:wrap; }
|
||||||
|
.page-heading h1 { font-size:30px; }
|
||||||
|
.protocol-badge { display:none; }
|
||||||
|
.workspace-footer { flex-wrap:wrap; }
|
||||||
|
.content-panel { padding:18px; }
|
||||||
|
.skip-link { left:16px; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
/** SFTP paths use POSIX separators, regardless of the OS running PHP. */
|
||||||
|
function normalizePath(string $path): string {
|
||||||
|
$parts = array_filter(explode('/', $path), fn($part) => $part !== '' && $part !== '.');
|
||||||
|
$stack = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if ($part === '..') {
|
||||||
|
array_pop($stack);
|
||||||
|
} else {
|
||||||
|
$stack[] = $part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '/' . implode('/', $stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like dirname(), but only '/' is a separator and the root stays '/'. */
|
||||||
|
function remoteDirname(string $path): string {
|
||||||
|
$path = rtrim($path, '/');
|
||||||
|
if ($path === '') return '/';
|
||||||
|
$separator = strrpos($path, '/');
|
||||||
|
if ($separator === false) return '.';
|
||||||
|
$parent = rtrim(substr($path, 0, $separator), '/');
|
||||||
|
return $parent === '' ? '/' : $parent;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
/** Shared navigation and page frame for the SFTP workspace. */
|
||||||
|
function workspaceEscape($value) {
|
||||||
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceAsset($root, $path) {
|
||||||
|
return workspaceEscape($root . $path . '?v=' . filemtime(__DIR__ . '/../' . $path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWorkspaceStart(array $page) {
|
||||||
|
$root = $page['root'];
|
||||||
|
$active = $page['active'] ?? 'files';
|
||||||
|
$guest = $page['guest'] ?? false;
|
||||||
|
$username = $_SESSION['uname'] ?? '';
|
||||||
|
$admin = !$guest && !empty($_SESSION['admin']);
|
||||||
|
$filesUrl = $root . 'content/ftp/index.php';
|
||||||
|
$nav = [
|
||||||
|
'files' => ['All files', '▱', $filesUrl],
|
||||||
|
'server' => ['Server status', '◴', $root . 'content/ftp/serverstat.php'],
|
||||||
|
'users' => ['User management', '♙', $root . 'content/adminpanel.php'],
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<body class="file-app">
|
||||||
|
<a class="skip-link" href="#workspace-content">Skip to content</a>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<a class="brand" href="<?= workspaceEscape($guest ? $root . 'index.php' : $filesUrl) ?>"><span class="brand-mark" aria-hidden="true">▤</span><span>USB RAID<span class="brand-caption">SFTP WORKSPACE</span></span></a>
|
||||||
|
<div class="eyebrow nav-heading"><?= $guest ? 'Shared workspace' : 'Workspace' ?></div>
|
||||||
|
<nav aria-label="Main navigation">
|
||||||
|
<?php if ($guest): ?>
|
||||||
|
<a class="nav-item active" href="#workspace-content" aria-current="page"><span aria-hidden="true">↗</span> Shared file</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($nav as $key => [$label, $icon, $url]): ?>
|
||||||
|
<?php if ($key !== 'files' && !$admin) continue; ?>
|
||||||
|
<a class="nav-item<?= $active === $key ? ' active' : '' ?>" href="<?= workspaceEscape($url) ?>"<?= $active === $key ? ' aria-current="page"' : '' ?>><span aria-hidden="true"><?= $icon ?></span> <?= $label ?></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="connection-card"><?php if (!empty($page['connected'])): ?><span class="status-dot"></span><?php endif; ?><?= $guest ? 'Shared access' : (!empty($page['connected']) ? 'Server connected' : 'Remote storage') ?><small><?= $guest ? 'Access through a shared link' : 'Your SFTP workspace' ?></small></div>
|
||||||
|
<?php if ($guest): ?>
|
||||||
|
<a class="nav-item" href="<?= workspaceEscape($root . 'index.php') ?>">Sign in to your workspace ↗</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a class="nav-item<?= $active === 'account' ? ' active' : '' ?>" href="<?= workspaceEscape($root . 'content/changepassword.php') ?>"<?= $active === 'account' ? ' aria-current="page"' : '' ?>>Account settings</a>
|
||||||
|
<div class="user-card"><span class="avatar"><?= workspaceEscape(strtoupper(substr($username, 0, 1))) ?></span><span class="user-name"><?= workspaceEscape($username) ?><small><?= $admin ? 'Administrator' : 'Member' ?></small></span><a href="<?= workspaceEscape($root . 'content/logout.php') ?>" title="Sign out" aria-label="Sign out">↗</a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class="workspace">
|
||||||
|
<header class="topbar"><span><?= $guest ? 'Shared workspace' : 'Workspace' ?><span class="topbar-divider">/</span><strong><?= workspaceEscape($page['section']) ?></strong></span><button id="themeToggle" class="btn btn-sm theme-toggle"><i aria-hidden="true"></i><span id="themeText">Theme</span></button></header>
|
||||||
|
<main class="file-main" id="workspace-content" tabindex="-1">
|
||||||
|
<div class="page-heading"><div><div class="eyebrow"><?= workspaceEscape($page['eyebrow']) ?></div><h1><?= workspaceEscape($page['title']) ?><span class="heading-dot">.</span></h1><p><?= workspaceEscape($page['description']) ?></p></div>
|
||||||
|
<?php if (isset($page['back'])): ?><a class="btn heading-back" href="<?= workspaceEscape($page['back']) ?>">← Back to files</a><?php elseif (!empty($page['connected'])): ?><span class="protocol-badge"><span class="status-dot"></span> SFTP</span><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="workspace-content <?= workspaceEscape($page['class'] ?? '') ?>">
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWorkspaceEnd() {
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<footer class="workspace-footer"><span>USB RAID Array <span aria-hidden="true">·</span> Developed by Michal Sedlák</span><a href="https://github.com/michalcz10/USB-RAID-Array" target="_blank" rel="noopener noreferrer">View on GitHub ↗</a></footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
+12
-58
@@ -15,68 +15,22 @@ if (empty($_SESSION)) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cz">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Login</title>
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
|
<title>Sign in · USB RAID</title>
|
||||||
<link rel="stylesheet" href="css/bootstrap.css">
|
<link rel="icon" href="img/favicon.ico">
|
||||||
<link rel="stylesheet" href="css/modern.css">
|
<link rel="stylesheet" href="css/modern.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
||||||
<link rel="stylesheet" href="css/index.css">
|
<link rel="stylesheet" href="css/index.css">
|
||||||
<script src="js/bootstrap.bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="container-fluid text-center">
|
|
||||||
<div class="d-flex justify-content-end p-3">
|
|
||||||
<button id="themeToggle" class="btn btn-sm theme-toggle">
|
|
||||||
<i class="bi"></i>
|
|
||||||
<span id="themeText"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="custom-container dark-box">
|
|
||||||
<header class="row border-bottom m-5">
|
|
||||||
<h1>USB RAID Array</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<section class="row m-3">
|
|
||||||
<article class="col border border-2 border-primary rounded p-2 fixed surface-panel">
|
|
||||||
<form action="content/login.php" method="post">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="uname">Username:</label>
|
|
||||||
<br>
|
|
||||||
<input type="text" name="uname" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="pswd">Password:</label>
|
|
||||||
<br>
|
|
||||||
<input type="password" name="pswd" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<input type="submit" class="btn btn-primary" value="Login">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3">
|
|
||||||
<span class="text-muted">Developed by Michal Sedlák</span>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
|
|
||||||
<img src="img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
|
|
||||||
</a>
|
|
||||||
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
|
|
||||||
<img src="img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/theme.js"></script>
|
<script src="js/theme.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="login-page">
|
||||||
|
<header class="login-top"><a class="login-brand" href="index.php"><span>▤</span> USB RAID</a><button id="themeToggle" class="btn btn-sm"><i aria-hidden="true"></i><span id="themeText">Theme</span></button></header>
|
||||||
|
<main class="login-layout">
|
||||||
|
<section class="login-story"><div class="eyebrow">Your storage. Your workspace.</div><h1>A home for<br>every file<span>.</span></h1><p>Connect to your RAID storage and keep your files within reach. One simple workspace, powered by SFTP.</p><div class="storage-illustration" aria-hidden="true"><div class="storage-top">USB RAID <span>SFTP</span></div><div class="storage-drive"><b>01</b><span></span><i></i></div><div class="storage-drive"><b>02</b><span></span><i></i></div><div class="storage-drive"><b>03</b><span></span><i></i></div><div class="storage-bottom">REMOTE STORAGE / LOCAL CONTROL</div></div></section>
|
||||||
|
<section class="login-card"><div class="eyebrow">Welcome back</div><h2>Sign in to your workspace</h2><p class="text-muted">Use your account to access your files.</p><form action="content/login.php" method="post"><div class="mb-3"><label for="uname">Username</label><input id="uname" name="uname" class="form-control" autocomplete="username" placeholder="Enter your username" required></div><div class="mb-4"><label for="pswd">Password</label><input id="pswd" type="password" name="pswd" class="form-control" autocomplete="current-password" placeholder="Enter your password" required></div><button type="submit" class="btn btn-primary">Sign in <span aria-hidden="true">→</span></button></form><div class="login-note">Need access? Contact your administrator.</div></section>
|
||||||
|
</main>
|
||||||
|
<footer class="login-footer"><span>Developed by Michal Sedlák</span><a href="https://github.com/michalcz10/USB-RAID-Array" target="_blank" rel="noopener noreferrer">Project on GitHub ↗</a></footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+22
-36
@@ -1,40 +1,26 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
(() => {
|
||||||
const themeToggle = document.getElementById('themeToggle');
|
const preference = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
const html = document.documentElement;
|
let saved;
|
||||||
const themeText = document.getElementById('themeText');
|
try { saved = localStorage.getItem('theme'); } catch (_) { /* Storage may be disabled. */ }
|
||||||
const themeIcon = themeToggle.querySelector('.bi');
|
|
||||||
|
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
html.setAttribute('data-bs-theme', theme);
|
document.documentElement.dataset.theme = theme;
|
||||||
document.body.classList.remove('theme-light', 'theme-dark');
|
const button = document.getElementById('themeToggle');
|
||||||
document.body.classList.add('theme-' + theme);
|
if (button) {
|
||||||
localStorage.setItem('theme', theme);
|
button.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`);
|
||||||
|
const label = document.getElementById('themeText');
|
||||||
if (theme === 'dark') {
|
if (label) label.textContent = theme === 'dark' ? 'Light mode' : 'Dark mode';
|
||||||
themeText.textContent = 'Light Mode';
|
const icon = button.querySelector('i');
|
||||||
themeIcon.className = 'bi bi-sun';
|
if (icon) { icon.textContent = theme === 'dark' ? '☀' : '◐'; icon.setAttribute('aria-hidden', 'true'); }
|
||||||
themeToggle.classList.remove('btn-dark');
|
|
||||||
themeToggle.classList.add('btn-light');
|
|
||||||
} else {
|
|
||||||
themeText.textContent = 'Dark Mode';
|
|
||||||
themeIcon.className = 'bi bi-moon';
|
|
||||||
themeToggle.classList.remove('btn-light');
|
|
||||||
themeToggle.classList.add('btn-dark');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setTheme(saved === 'dark' || saved === 'light' ? saved : preference.matches ? 'dark' : 'light');
|
||||||
const savedTheme = localStorage.getItem('theme');
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
setTheme(document.documentElement.dataset.theme);
|
||||||
|
document.getElementById('themeToggle')?.addEventListener('click', () => {
|
||||||
if (savedTheme) {
|
saved = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
||||||
setTheme(savedTheme);
|
setTheme(saved);
|
||||||
} else {
|
try { localStorage.setItem('theme', saved); } catch (_) { /* Keep this session usable. */ }
|
||||||
setTheme(prefersDark ? 'dark' : 'light');
|
});
|
||||||
}
|
|
||||||
|
|
||||||
themeToggle.addEventListener('click', function() {
|
|
||||||
const currentTheme = html.getAttribute('data-bs-theme');
|
|
||||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
||||||
setTheme(newTheme);
|
|
||||||
});
|
});
|
||||||
});
|
preference.addEventListener('change', () => { if (saved !== 'dark' && saved !== 'light') setTheme(preference.matches ? 'dark' : 'light'); });
|
||||||
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Native dialogs supply focus containment, Escape and focus restoration.
|
||||||
|
document.addEventListener('click', event => {
|
||||||
|
const trigger = event.target.closest('[data-dialog]');
|
||||||
|
if (trigger) document.getElementById(trigger.dataset.dialog)?.showModal();
|
||||||
|
if (event.target.closest('[data-dialog-close]')) event.target.closest('dialog')?.close();
|
||||||
|
if (event.target instanceof HTMLDialogElement) {
|
||||||
|
const rect = event.target.getBoundingClientRect();
|
||||||
|
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) event.target.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../Web/includes/remotepaths.php';
|
||||||
|
|
||||||
|
$parents = [
|
||||||
|
'/' => '/',
|
||||||
|
'/etc' => '/',
|
||||||
|
'/etc/' => '/',
|
||||||
|
'/etc/file.txt' => '/etc',
|
||||||
|
'//etc//file.txt' => '//etc',
|
||||||
|
'/folder with spaces/item.txt' => '/folder with spaces',
|
||||||
|
'folder/file.txt' => 'folder',
|
||||||
|
'file.txt' => '.',
|
||||||
|
'/a\\b/file.txt' => '/a\\b', // Backslash is a valid POSIX filename character.
|
||||||
|
];
|
||||||
|
foreach ($parents as $path => $expected) {
|
||||||
|
if (remoteDirname($path) !== $expected) throw new RuntimeException('Incorrect parent for ' . $path);
|
||||||
|
}
|
||||||
|
foreach (['/' => '/', '//etc/../' => '/', '/etc/../../' => '/', '/etc/./ssh/' => '/etc/ssh'] as $path => $expected) {
|
||||||
|
if (normalizePath($path) !== $expected) throw new RuntimeException('Incorrect normalized path for ' . $path);
|
||||||
|
}
|
||||||
|
// Round trips must remain correct on both Windows/XAMPP and Linux.
|
||||||
|
foreach (['/etc', '/mnt/raid', '/folder with spaces'] as $directory) {
|
||||||
|
$parent = normalizePath(remoteDirname($directory));
|
||||||
|
if ($parent !== ($directory === '/mnt/raid' ? '/mnt' : '/')) throw new RuntimeException('Invalid navigation round trip');
|
||||||
|
}
|
||||||
|
// ZIP paths keep the top-level directory name when it is directly under root.
|
||||||
|
foreach (['/etc' => '/etc/config.txt', '/mnt/raid' => '/mnt/raid/config.txt'] as $base => $file) {
|
||||||
|
$relative = substr($file, strlen(rtrim(remoteDirname($base), '/')) + 1);
|
||||||
|
if ($relative !== ($base === '/etc' ? 'etc/config.txt' : 'raid/config.txt')) throw new RuntimeException('Incorrect ZIP member path');
|
||||||
|
}
|
||||||
|
echo "Remote paths: POSIX parents, normalization, root navigation and ZIP paths passed.\n";
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
|
||||||
|
require_once __DIR__ . '/../Web/includes/workspace.php';
|
||||||
|
|
||||||
|
// Treat warnings and deprecations as failures, including when rendering missing data.
|
||||||
|
set_error_handler(function ($severity, $message, $file, $line) {
|
||||||
|
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||||
|
});
|
||||||
|
function check($condition, $message) {
|
||||||
|
if (!$condition) throw new RuntimeException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$disk = parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/mapper/vg-root 1048576 262144 786432 25% /\n");
|
||||||
|
check($disk['available_data'] && $disk['size'] === '1 GiB' && $disk['usage'] === '25%', 'Parse POSIX df sizes and usage');
|
||||||
|
foreach ([false, null, '', 'df: /dev/sdb1: No such file or directory', "Filesystem 1024-blocks Used Available Capacity Mounted on\n"] as $output) {
|
||||||
|
check(!parseDiskData($output)['available_data'], 'Missing disk output must be unavailable');
|
||||||
|
}
|
||||||
|
$healthy = "Personalities : [raid1] [raid5]\nmd0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n\nunused devices: <none>\n";
|
||||||
|
check(parseRaidStatus($healthy)['status'] === 'healthy', 'Three-disk RAID5 should be healthy');
|
||||||
|
check(parseRaidStatus(str_replace('[UUU]', '[U_U]', $healthy))['status'] === 'degraded', 'Missing RAID member should be degraded');
|
||||||
|
check(parseRaidStatus($healthy . "md1 : active raid1 sdd[0] sde[1]\n 1000 blocks [2/1] [U_]\n")['status'] === 'degraded', 'One failed array must make overall status degraded');
|
||||||
|
check(parseRaidStatus(str_replace('[UUU]', "[UUU]\n resync = 42.0%", $healthy))['status'] === 'syncing', 'RAID resync is visible');
|
||||||
|
check(parseRaidStatus("md0 : active raid0 sda[0] sdb[1]\n 1000 blocks super 1.2 512k chunks\n")['status'] === 'active', 'RAID0 does not require a redundancy bitmap');
|
||||||
|
check(parseRaidStatus("md0 : inactive sda[0](S)\n 1000 blocks\n")['status'] === 'inactive', 'Inactive array remains inactive');
|
||||||
|
check(parseRaidStatus("Personalities : [raid1]\nunused devices: <none>\n")['status'] === 'not configured', 'No configured Linux MD array');
|
||||||
|
check(parseRaidStatus(false)['status'] === 'unavailable', 'Unreadable mdstat is unavailable');
|
||||||
|
$resources = parseSystemStats('97,5', '1024,256,768,0,0,0');
|
||||||
|
check($resources['cpu']['usage'] === '2.5' && $resources['memory']['usage'] === '25%' && $resources['memory']['swap_usage'] === '0%', 'Resource values and zero swap');
|
||||||
|
check(!parseSystemStats(false, false)['cpu']['available_data'], 'Missing CPU output must not show 100%');
|
||||||
|
check(!parseSystemStats('not a number', '')['memory']['available_data'], 'Missing memory output must be unavailable');
|
||||||
|
|
||||||
|
// Check remote command construction and path quoting with a harmless stub.
|
||||||
|
$ssh = new class {
|
||||||
|
public $commands = [];
|
||||||
|
function exec($command) { $this->commands[] = $command; return false; }
|
||||||
|
};
|
||||||
|
$defaultPath = "/storage/owner's files";
|
||||||
|
$systemData = getSystemData($ssh, $defaultPath);
|
||||||
|
check(in_array("LC_ALL=C df -Pk -- '/' 2>/dev/null", $ssh->commands, true), 'System filesystem uses root instead of a device name');
|
||||||
|
check(in_array("LC_ALL=C df -Pk -- '/storage/owner'\\''s files' 2>/dev/null", $ssh->commands, true), 'Storage path is quoted for remote Linux even on Windows');
|
||||||
|
|
||||||
|
// Exercise the actual page template with missing output and with valid output.
|
||||||
|
$source = file_get_contents(__DIR__ . '/../Web/content/ftp/serverstat.php');
|
||||||
|
$template = substr($source, strpos($source, '$monitors ='));
|
||||||
|
foreach ([false, true] as $available) {
|
||||||
|
if ($available) {
|
||||||
|
$systemData['storage'] = ['system' => $disk, 'data' => $disk];
|
||||||
|
$systemData['resources'] = $resources;
|
||||||
|
$systemData['raid'] = parseRaidStatus($healthy);
|
||||||
|
}
|
||||||
|
ob_start();
|
||||||
|
eval($template);
|
||||||
|
$html = ob_get_clean();
|
||||||
|
check(strpos($html, 'Server status') !== false, 'Status page renders');
|
||||||
|
check($available ? strpos($html, '1 GiB') !== false : strpos($html, 'Storage statistics are unavailable') !== false, 'Template handles available and missing data');
|
||||||
|
}
|
||||||
|
echo "Server statistics: parser, RAID, command quoting and page rendering checks passed.\n";
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../Web/includes/workspace.php';
|
||||||
|
require_once __DIR__ . '/../Web/includes/remotepaths.php';
|
||||||
|
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
|
||||||
|
set_error_handler(function ($severity, $message, $file, $line) {
|
||||||
|
if (error_reporting() & $severity) throw new ErrorException($message, 0, $severity, $file, $line);
|
||||||
|
});
|
||||||
|
function expectWorkspace($condition, $message) {
|
||||||
|
if (!$condition) throw new RuntimeException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWorkspaceFixture($name, $admin = true, $defaultPath = '/mnt/raid', $currentPath = '/mnt/raid/Workspace') {
|
||||||
|
$_SESSION = ['uname' => 'michal', 'admin' => $admin, 'upPer' => true, 'downPer' => true, 'delPer' => true];
|
||||||
|
$filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentDir = $currentPath;
|
||||||
|
$fileExtension = 'png'; $fileSize = 245760; $mimeType = 'image/png';
|
||||||
|
$isImage = true; $isVideo = false; $isAudio = false;
|
||||||
|
$editable = true; $showLineNumbers = true; $content = "{\n \"workspace\": \"USB RAID\",\n \"status\": \"connected\"\n}\n";
|
||||||
|
$message = ''; $messageType = ''; $message_type = ''; $shareCreated = false; $shareError = '';
|
||||||
|
$share = ['expiration' => '2030-01-01']; $downloadAllowed = false; $token = 'test-only';
|
||||||
|
$items = ['Documents', 'Photos', 'README.md', 'Example.pdf'];
|
||||||
|
$imageExtensions = ['png']; $videoExtensions = []; $audioExtensions = []; $editableExtensions = ['md']; $archiveExtensions = [];
|
||||||
|
$sftp = new class {
|
||||||
|
function is_dir($name) { return strpos($name, '.') === false; }
|
||||||
|
function stat($path) { return ['size' => 245760]; }
|
||||||
|
};
|
||||||
|
$result = new class {
|
||||||
|
private $rows = [
|
||||||
|
['uname' => 'michal', 'admin' => true, 'defPath' => '/mnt/raid', 'delPer' => true, 'downPer' => true, 'upPer' => true],
|
||||||
|
['uname' => 'guest', 'admin' => false, 'defPath' => '/mnt/raid/shared', 'delPer' => false, 'downPer' => true, 'upPer' => false],
|
||||||
|
];
|
||||||
|
function fetch_assoc() { return array_shift($this->rows); }
|
||||||
|
};
|
||||||
|
$systemData = [
|
||||||
|
'storage' => array_fill_keys(['system', 'data'], parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/md0 1048576 262144 786432 25% /mnt/raid\n")),
|
||||||
|
'raid' => parseRaidStatus("md0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n"),
|
||||||
|
'resources' => parseSystemStats('93', '8192,2048,6144,1024,0,1024'), 'lastUpdate' => '2026-10-16 10:00:00',
|
||||||
|
];
|
||||||
|
$top = in_array($name, ['adminpanel', 'changepassword'], true);
|
||||||
|
$source = file_get_contents(__DIR__ . '/../Web/content/' . ($top ? '' : 'ftp/') . $name . '.php');
|
||||||
|
if ($name === 'serverstat') {
|
||||||
|
$start = strpos($source, '$monitors =');
|
||||||
|
eval(substr($source, $start, strpos($source, '?>', $start) - $start));
|
||||||
|
}
|
||||||
|
$start = strpos($source, '<!DOCTYPE html>');
|
||||||
|
$template = substr($source, $start, strpos($source, '</html>', $start) + 7 - $start);
|
||||||
|
ob_start(); eval('?>' . $template); return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview = in_array('--preview', $argv, true);
|
||||||
|
$directory = __DIR__ . '/../Web/.layout-preview';
|
||||||
|
if ($preview && !is_dir($directory)) mkdir($directory);
|
||||||
|
$pages = ['index', 'serverstat', 'adminpanel', 'changepassword', 'open', 'view', 'pdf', 'shared'];
|
||||||
|
foreach ($pages as $name) {
|
||||||
|
$html = renderWorkspaceFixture($name);
|
||||||
|
$dom = new DOMDocument();
|
||||||
|
@$dom->loadHTML($html);
|
||||||
|
$xpath = new DOMXPath($dom);
|
||||||
|
expectWorkspace($xpath->query('//body')->length === 1, "$name has one body");
|
||||||
|
expectWorkspace($xpath->query('//main')->length === 1, "$name has one main region");
|
||||||
|
expectWorkspace($xpath->query('//aside[contains(@class,"sidebar")]')->length === 1, "$name has shared sidebar");
|
||||||
|
expectWorkspace($xpath->query('//*[@id="themeToggle"]')->length === 1, "$name has one theme switch");
|
||||||
|
expectWorkspace($xpath->query('//a[@aria-current="page"]')->length === 1, "$name has one active navigation item");
|
||||||
|
expectWorkspace($xpath->query('//main//footer')->length === 1, "$name footer is inside the main region");
|
||||||
|
if ($name === 'shared') expectWorkspace(strpos($html, 'User management') === false && strpos($html, 'Account settings') === false, 'Public shared preview does not expose account navigation');
|
||||||
|
if ($preview) {
|
||||||
|
$base = $name === 'adminpanel' || $name === 'changepassword' ? '../content/' : '../content/ftp/';
|
||||||
|
$html = str_replace('<head>', '<head><base href="' . $base . '">', $html);
|
||||||
|
// Browser previews never request a real remote file or perform mutations.
|
||||||
|
$image = 'data:image/svg+xml,' . rawurlencode('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="380"><rect width="800" height="380" fill="#1c3053"/><circle cx="400" cy="150" r="70" fill="#2563eb"/><text x="400" y="290" text-anchor="middle" fill="white" font-size="28" font-family="sans-serif">USB RAID / Workspace preview</text></svg>');
|
||||||
|
$html = preg_replace('/src="(?:view|shared)\.php\?[^"\n]+"/', 'src="' . $image . '"', $html);
|
||||||
|
$html = str_replace("const url = 'pdf.php?file=%2Fmnt%2Fraid%2FWorkspace%2FExample.pdf&stream=1';", "const url = '/Web/.layout-preview/sample.pdf';", $html);
|
||||||
|
file_put_contents($directory . '/' . $name . '.html', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$member = renderWorkspaceFixture('index', false);
|
||||||
|
expectWorkspace(strpos($member, 'User management') === false && strpos($member, '> Server status</a>') === false, 'Member navigation omits admin pages');
|
||||||
|
expectWorkspace(strpos($member, 'Account settings') !== false, 'Members can access account settings');
|
||||||
|
$rootChild = renderWorkspaceFixture('index', true, '/', '/etc');
|
||||||
|
expectWorkspace(strpos($rootChild, 'href="?path=%2F"') !== false, 'Parent link from a root child uses a POSIX slash');
|
||||||
|
expectWorkspace(strpos($rootChild, '%5C') === false, 'No Windows separator in remote links');
|
||||||
|
$root = renderWorkspaceFixture('index', true, '/', '/');
|
||||||
|
expectWorkspace(strpos($root, 'Parent folder') === false, 'Root has no parent-folder link');
|
||||||
|
expectWorkspace(strpos($root, 'href="?path=%2FDocuments"') !== false, 'Root children use a single leading slash');
|
||||||
|
if ($preview) {
|
||||||
|
// One-page PDF fixture for viewer navigation and fullscreen checks.
|
||||||
|
$stream = "BT /F1 24 Tf 60 700 Td (USB RAID workspace preview) Tj ET";
|
||||||
|
$objects = ['<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', '<< /Length ' . strlen($stream) . ">>\nstream\n" . $stream . "\nendstream"];
|
||||||
|
$pdf = "%PDF-1.4\n"; $offsets = [0];
|
||||||
|
foreach ($objects as $i => $object) { $offsets[] = strlen($pdf); $pdf .= ($i + 1) . " 0 obj\n$object\nendobj\n"; }
|
||||||
|
$xref = strlen($pdf); $pdf .= "xref\n0 6\n0000000000 65535 f \n";
|
||||||
|
foreach (array_slice($offsets, 1) as $offset) $pdf .= sprintf("%010d 00000 n \n", $offset);
|
||||||
|
$pdf .= "trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n$xref\n%%EOF";
|
||||||
|
file_put_contents($directory . '/sample.pdf', $pdf);
|
||||||
|
}
|
||||||
|
echo "Workspace: all eight page templates, navigation roles and shared layout checks passed.\n";
|
||||||
Reference in New Issue
Block a user