mirror of
https://github.com/nosial/FileServer.git
synced 2025-06-06 13:38:36 +00:00
Add configuration classes, enums, and database handling for FileServer
Some checks failed
Docker CI/CD / build-and-save (push) Has been cancelled
Docker CI/CD / build-and-push (push) Has been cancelled
CI / release (push) Has been cancelled
CI / debug (push) Has been cancelled
CI / release_executable (push) Has been cancelled
CI / debug_executable (push) Has been cancelled
CI / check-phpunit (push) Has been cancelled
CI / check-phpdoc (push) Has been cancelled
CI / generate-phpdoc (push) Has been cancelled
CI / test (push) Has been cancelled
CI / release-documentation (push) Has been cancelled
CI / release-artifacts (push) Has been cancelled
Some checks failed
Docker CI/CD / build-and-save (push) Has been cancelled
Docker CI/CD / build-and-push (push) Has been cancelled
CI / release (push) Has been cancelled
CI / debug (push) Has been cancelled
CI / release_executable (push) Has been cancelled
CI / debug_executable (push) Has been cancelled
CI / check-phpunit (push) Has been cancelled
CI / check-phpdoc (push) Has been cancelled
CI / generate-phpdoc (push) Has been cancelled
CI / test (push) Has been cancelled
CI / release-documentation (push) Has been cancelled
CI / release-artifacts (push) Has been cancelled
This commit is contained in:
parent
8db44e90d5
commit
72da412737
45 changed files with 3837 additions and 10 deletions
76
.github/workflows/docker.yml
vendored
Normal file
76
.github/workflows/docker.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
name: Docker CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-save:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
IMAGE_NAME=$(echo ${{ github.repository }} | cut -d'/' -f2)
|
||||
docker build -t $IMAGE_NAME:${{ github.sha }} .
|
||||
|
||||
- name: Save Docker image as .tar artifact
|
||||
run: |
|
||||
IMAGE_NAME=$(echo ${{ github.repository }} | cut -d'/' -f2)
|
||||
docker save $IMAGE_NAME:${{ github.sha }} > docker-image-${{ github.sha }}.tar
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-image-${{ github.sha }}
|
||||
path: docker-image-${{ github.sha }}.tar
|
||||
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'release'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
docker.io/your-dockerhub-username:${{ github.event.release.tag_name }}
|
||||
docker.io/your-dockerhub-username:latest
|
||||
push: true
|
6
.github/workflows/ncc_workflow.yml
vendored
6
.github/workflows/ncc_workflow.yml
vendored
|
@ -57,7 +57,7 @@ jobs:
|
|||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: build/release/net.nosial.file_server.ncc
|
||||
path: build/release/net.nosial.fileserver.ncc
|
||||
debug:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
|
@ -106,7 +106,7 @@ jobs:
|
|||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug
|
||||
path: build/debug/net.nosial.file_server.ncc
|
||||
path: build/debug/net.nosial.fileserver.ncc
|
||||
release_executable:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
|
@ -327,7 +327,7 @@ jobs:
|
|||
|
||||
- name: Install NCC packages
|
||||
run: |
|
||||
ncc package install --package="release/net.nosial.file_server.ncc" --build-source --reinstall -y --log-level debug
|
||||
ncc package install --package="release/net.nosial.fileserver.ncc" --build-source --reinstall -y --log-level debug
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
run: |
|
||||
|
|
22
.idea/dataSources.xml
generated
Normal file
22
.idea/dataSources.xml
generated
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="fileserver@127.0.0.1" uuid="4986c4c2-f51c-4b89-b812-58ec37d71c36">
|
||||
<driver-ref>mariadb</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mariadb://127.0.0.1:3306/fileserver</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="database.introspection.mysql.dbe5060" value="true" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="Sqlite Database" uuid="4bb9a5ec-5d42-4f5d-bb52-f1bd0268fe17">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/src/FileServer/Classes/Resources/database.sqlite</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
1
.idea/php.xml
generated
1
.idea/php.xml
generated
|
@ -12,6 +12,7 @@
|
|||
</component>
|
||||
<component name="PhpIncludePathManager">
|
||||
<include_path>
|
||||
<path value="/usr/share/ncc" />
|
||||
<path value="/var/ncc/packages/com.symfony.uid=v7.2.0" />
|
||||
<path value="/var/ncc/packages/net.nosial.configlib=1.1.8" />
|
||||
<path value="/var/ncc/packages/net.nosial.loglib2=1.0.3" />
|
||||
|
|
9
.idea/sqldialects.xml
generated
Normal file
9
.idea/sqldialects.xml
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/src/FileServer/Classes/Database.php" dialect="MySQL" />
|
||||
<file url="file://$PROJECT_DIR$/src/FileServer/Classes/IndexStorageManager.php" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/FileServer/Classes/Resources/statistics.sql" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/FileServer/Classes/Resources/uploads.sql" dialect="MariaDB" />
|
||||
</component>
|
||||
</project>
|
14
.idea/webResources.xml
generated
Normal file
14
.idea/webResources.xml
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WebResourcesPaths">
|
||||
<contentEntries>
|
||||
<entry url="file://$PROJECT_DIR$">
|
||||
<entryData>
|
||||
<resourceRoots>
|
||||
<path value="file://$PROJECT_DIR$/src/FileServer/Classes/Resources" />
|
||||
</resourceRoots>
|
||||
</entryData>
|
||||
</entry>
|
||||
</contentEntries>
|
||||
</component>
|
||||
</project>
|
107
Dockerfile
Normal file
107
Dockerfile
Normal file
|
@ -0,0 +1,107 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Dockerfile for PHP 8.3 + FPM with Cron support and Supervisor
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Base image: Official PHP 8.3 with FPM
|
||||
FROM php:8.3-fpm AS base
|
||||
|
||||
# ----------------------------- Metadata labels ------------------------------
|
||||
LABEL maintainer="Netkas <netkas@n64.cc>" \
|
||||
version="1.0" \
|
||||
description="FileServer Docker image based off PHP 8.3 FPM and NCC" \
|
||||
application="FileServer" \
|
||||
base_image="php:8.3-fpm"
|
||||
|
||||
# Environment variable for non-interactive installations
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# ----------------------------- System Dependencies --------------------------
|
||||
# Update system packages and install required dependencies in one step
|
||||
RUN apt-get update -yqq && apt-get install -yqq --no-install-recommends \
|
||||
git \
|
||||
libpq-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
make \
|
||||
wget \
|
||||
gnupg \
|
||||
cron \
|
||||
supervisor \
|
||||
mariadb-client \
|
||||
libcurl4-openssl-dev \
|
||||
python3-colorama \
|
||||
nginx \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ----------------------------- PHP Extensions -------------------------------
|
||||
# Install PHP extensions and enable additional ones
|
||||
RUN docker-php-ext-install -j$(nproc) \
|
||||
pdo \
|
||||
pdo_mysql \
|
||||
pdo_sqlite \
|
||||
mysqli \
|
||||
curl \
|
||||
opcache \
|
||||
sockets \
|
||||
zip \
|
||||
pcntl
|
||||
|
||||
# ----------------------------- Additional Tools -----------------------------
|
||||
# Install Phive (Package Manager for PHAR libraries) and global tools in one step
|
||||
RUN wget -O /usr/local/bin/phive https://phar.io/releases/phive.phar && \
|
||||
wget -O /usr/local/bin/phive.asc https://phar.io/releases/phive.phar.asc && \
|
||||
gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 && \
|
||||
gpg --verify /usr/local/bin/phive.asc /usr/local/bin/phive && \
|
||||
chmod +x /usr/local/bin/phive && \
|
||||
phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C
|
||||
|
||||
# ----------------------------- Clone and Build NCC --------------------------
|
||||
# Clone the NCC repository, build the project, and install it
|
||||
RUN git clone https://git.n64.cc/nosial/ncc.git && \
|
||||
cd ncc && \
|
||||
make redist && \
|
||||
NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) && \
|
||||
if [ -z "$NCC_DIR" ]; then \
|
||||
echo "NCC build directory not found"; \
|
||||
exit 1; \
|
||||
fi && \
|
||||
php "$NCC_DIR/INSTALL" --auto && \
|
||||
cd .. && rm -rf ncc
|
||||
|
||||
# ----------------------------- Project Build ---------------------------------
|
||||
# Set build directory and copy pre-needed project files
|
||||
WORKDIR /tmp/build
|
||||
COPY . .
|
||||
|
||||
RUN ncc build --config release --build-source --log-level debug && \
|
||||
ncc package install --package=build/release/net.nosial.fileserver.ncc --build-source -y --log-level=debug
|
||||
|
||||
# Clean up
|
||||
RUN rm -rf /tmp/build && rm -rf /var/www/html/*
|
||||
|
||||
# Copy over the required files
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY public/index.php /var/www/html/index.php
|
||||
RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html
|
||||
|
||||
# Copy Supervisor configuration
|
||||
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
# Copy the logging server script over
|
||||
COPY docker/logger.py /logger.py
|
||||
|
||||
# ----------------------------- Cleanup ---------------------
|
||||
WORKDIR /
|
||||
|
||||
# ----------------------------- Port Exposing ---------------------------------
|
||||
EXPOSE 8081
|
||||
|
||||
# UDP Logging Server
|
||||
ENV LOGLIB_UDP_ENABLED="true"
|
||||
ENV LOGLIB_UDP_HOST="127.0.0.1"
|
||||
ENV LOGLIB_UDP_PORT="5131"
|
||||
ENV LOGLIB_UDP_TRACE_FORMAT="full"
|
||||
ENV LOGLIB_CONSOLE_ENABLED="true"
|
||||
ENV LOGLIB_CONSOLE_TRACE_FORMAT="full"
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
2
Makefile
2
Makefile
|
@ -17,7 +17,7 @@ debug_executable:
|
|||
|
||||
|
||||
install: release
|
||||
ncc package install --package=build/release/net.nosial.file_server.ncc --skip-dependencies --build-source --reinstall -y --log-level $(LOG_LEVEL)
|
||||
ncc package install --package=build/release/net.nosial.fileserver.ncc --skip-dependencies --build-source --reinstall -y --log-level $(LOG_LEVEL)
|
||||
|
||||
test: release
|
||||
[ -f phpunit.xml ] || { echo "phpunit.xml not found"; exit 1; }
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<?php
|
||||
require 'ncc';
|
||||
import('net.nosial.file_server');
|
||||
import('net.nosial.fileserver');
|
||||
|
|
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
|
@ -0,0 +1,57 @@
|
|||
services:
|
||||
fileserver:
|
||||
container_name: fileserver
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080" # Unique port for File Server instance
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
shared_network:
|
||||
aliases:
|
||||
- fileserver
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./fileserver/config:/etc/config:z
|
||||
- ./fileserver/uploads:/etc/fileserver:z
|
||||
- ./fileserver/logs:/var/log:z
|
||||
environment:
|
||||
# No need to change these values
|
||||
LOG_LEVEL: ${LOG_LEVEL:-debug}
|
||||
CONFIGLIB_PATH: /etc/config
|
||||
|
||||
mariadb:
|
||||
container_name: fileserver_mariadb
|
||||
image: mariadb:10.5
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-fileserver_root}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-fileserver}
|
||||
MYSQL_USER: ${MYSQL_USER:-fileserver}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-fileserver}
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
networks:
|
||||
- shared_network
|
||||
ports:
|
||||
- "3308:3306"
|
||||
expose:
|
||||
- "3306"
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin", "ping", "-h", "fileserver_mariadb", "-u", "${MYSQL_USER:-fileserver}", "-p${MYSQL_PASSWORD:-fileserver}" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
mariadb_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
shared_network:
|
||||
name: fileserver_network
|
||||
driver: bridge
|
346
docker/logger.py
Normal file
346
docker/logger.py
Normal file
|
@ -0,0 +1,346 @@
|
|||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from queue import Queue, Empty
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass
|
||||
import colorama
|
||||
from colorama import Fore, Back, Style
|
||||
|
||||
# Initialize colorama for cross-platform color support
|
||||
colorama.init()
|
||||
|
||||
|
||||
class LogLevel(str, Enum):
|
||||
DEBUG = "DBG"
|
||||
VERBOSE = "VRB"
|
||||
INFO = "INFO"
|
||||
WARNING = "WRN"
|
||||
ERROR = "ERR"
|
||||
CRITICAL = "CRT"
|
||||
|
||||
@classmethod
|
||||
def to_python_level(cls, level: str) -> int:
|
||||
return {
|
||||
cls.DEBUG: logging.DEBUG,
|
||||
cls.VERBOSE: logging.DEBUG,
|
||||
cls.INFO: logging.INFO,
|
||||
cls.WARNING: logging.WARNING,
|
||||
cls.ERROR: logging.ERROR,
|
||||
cls.CRITICAL: logging.CRITICAL
|
||||
}.get(level, logging.INFO)
|
||||
|
||||
@classmethod
|
||||
def get_color(cls, level: str) -> str:
|
||||
return {
|
||||
cls.DEBUG: Fore.CYAN,
|
||||
cls.VERBOSE: Fore.BLUE,
|
||||
cls.INFO: Fore.GREEN,
|
||||
cls.WARNING: Fore.YELLOW,
|
||||
cls.ERROR: Fore.RED,
|
||||
cls.CRITICAL: Fore.RED + Back.WHITE
|
||||
}.get(level, Fore.WHITE)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StackFrame:
|
||||
file: Optional[str]
|
||||
line: Optional[int]
|
||||
function: Optional[str]
|
||||
args: Optional[List[Any]]
|
||||
class_name: Optional[str]
|
||||
call_type: str = 'static'
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'StackFrame':
|
||||
return cls(
|
||||
file=str(data.get('file')) if data.get('file') else None,
|
||||
line=int(data['line']) if data.get('line') is not None else None,
|
||||
function=str(data.get('function')) if data.get('function') else None,
|
||||
args=data.get('args'),
|
||||
class_name=str(data.get('class')) if data.get('class') else None,
|
||||
call_type=str(data.get('callType', 'static'))
|
||||
)
|
||||
|
||||
def format(self) -> str:
|
||||
location = f"{self.file or '?'}:{self.line or '?'}"
|
||||
if self.class_name:
|
||||
call = f"{self.class_name}{self.call_type}{self.function or ''}"
|
||||
else:
|
||||
call = self.function or ''
|
||||
|
||||
args_str = ""
|
||||
if self.args:
|
||||
args_str = f"({', '.join(str(arg) for arg in self.args)})"
|
||||
|
||||
return f"{Fore.BLUE}{call}{Style.RESET_ALL}{args_str} in {Fore.CYAN}{location}{Style.RESET_ALL}"
|
||||
|
||||
|
||||
class ExceptionDetails:
|
||||
def __init__(self, name: str, message: str, code: Optional[int],
|
||||
file: Optional[str], line: Optional[int],
|
||||
trace: List[StackFrame], previous: Optional['ExceptionDetails']):
|
||||
self.name = name
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.file = file
|
||||
self.line = line
|
||||
self.trace = trace
|
||||
self.previous = previous
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> Optional['ExceptionDetails']:
|
||||
if not data:
|
||||
return None
|
||||
|
||||
trace = []
|
||||
if 'trace' in data and isinstance(data['trace'], list):
|
||||
trace = [StackFrame.from_dict(frame) for frame in data['trace']
|
||||
if isinstance(frame, dict)]
|
||||
|
||||
previous = None
|
||||
if 'previous' in data and isinstance(data['previous'], dict):
|
||||
previous = cls.from_dict(data['previous'])
|
||||
|
||||
return cls(
|
||||
name=str(data.get('name', '')),
|
||||
message=str(data.get('message', '')),
|
||||
code=int(data['code']) if data.get('code') is not None else None,
|
||||
file=str(data.get('file')) if data.get('file') else None,
|
||||
line=int(data['line']) if data.get('line') is not None else None,
|
||||
trace=trace,
|
||||
previous=previous
|
||||
)
|
||||
|
||||
def format(self, level: int = 0) -> str:
|
||||
indent = " " * level
|
||||
parts = []
|
||||
|
||||
# Exception header
|
||||
header = f"{indent}{Fore.RED}{self.name}"
|
||||
if self.code is not None and 0:
|
||||
header += f" {Fore.YELLOW}:{self.code}{Style.RESET_ALL}"
|
||||
|
||||
# Message
|
||||
header += f"{Fore.WHITE}:{Style.RESET_ALL} {self.message}"
|
||||
|
||||
# Location
|
||||
if self.file and self.line:
|
||||
header += f"{Fore.WHITE} at {Style.RESET_ALL}{self.file}:{self.line}"
|
||||
|
||||
parts.append(header)
|
||||
|
||||
# Stack trace
|
||||
if self.trace:
|
||||
parts.append(f"{indent}{Fore.WHITE}Stack trace:{Style.RESET_ALL}")
|
||||
for frame in self.trace:
|
||||
parts.append(f"{indent} → {frame.format()}")
|
||||
|
||||
# Previous exception
|
||||
if self.previous:
|
||||
parts.append(f"{indent}{Fore.YELLOW}Caused by:{Style.RESET_ALL}")
|
||||
parts.append(self.previous.format(level + 1))
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
class ColoredLogger(logging.Logger):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
self.formatter = logging.Formatter(
|
||||
f'%(asctime)s {Fore.WHITE}[%(levelname)s]{Style.RESET_ALL} %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(self.formatter)
|
||||
self.addHandler(console_handler)
|
||||
|
||||
|
||||
class MultiProtocolServer:
|
||||
def __init__(self, host: str, port: int, working_directory: str):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.working_directory = working_directory
|
||||
self.log_queue: Queue = Queue()
|
||||
self.current_date = datetime.now().strftime('%Y-%m-%d')
|
||||
self.log_file = None
|
||||
self.stop_event = threading.Event()
|
||||
|
||||
os.makedirs(self.working_directory, exist_ok=True)
|
||||
|
||||
# Set up colored logging
|
||||
logging.setLoggerClass(ColoredLogger)
|
||||
self.logger = logging.getLogger("MultiProtocolServer")
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
def _handle_log_event(self, data: Dict[str, Any], address: tuple) -> None:
|
||||
"""Process and format a structured log event with colors and proper formatting."""
|
||||
try:
|
||||
app_name = data.get('application_name', 'Unknown')
|
||||
timestamp = data.get('timestamp')
|
||||
if timestamp:
|
||||
try:
|
||||
timestamp = datetime.fromtimestamp(int(timestamp))
|
||||
timestamp = timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except (ValueError, TypeError):
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
level = data.get('level', 'INFO')
|
||||
message = data.get('message', '')
|
||||
|
||||
# Format the log message with colors
|
||||
color = LogLevel.get_color(level)
|
||||
log_message = f"{color}[{app_name}]{Style.RESET_ALL} {message}"
|
||||
|
||||
# Handle exception if present
|
||||
exception_data = data.get('exception')
|
||||
if exception_data:
|
||||
exception = ExceptionDetails.from_dict(exception_data)
|
||||
if exception:
|
||||
log_message += f"\n{exception.format()}"
|
||||
|
||||
# Log with appropriate level
|
||||
python_level = LogLevel.to_python_level(level)
|
||||
self.logger.log(python_level, log_message)
|
||||
|
||||
# Add to log queue for file logging
|
||||
self.log_queue.put({
|
||||
"timestamp": timestamp,
|
||||
"address": address,
|
||||
"data": data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing log event: {e}", exc_info=True)
|
||||
|
||||
def _handle_data(self, data: bytes, address: tuple) -> None:
|
||||
"""Process incoming data and attempt to parse as JSON."""
|
||||
try:
|
||||
decoded_data = data.decode('utf-8').strip()
|
||||
|
||||
try:
|
||||
json_data = json.loads(decoded_data)
|
||||
# Handle structured log event
|
||||
self._handle_log_event(json_data, address)
|
||||
except json.JSONDecodeError:
|
||||
# Log raw data if not valid JSON
|
||||
self.logger.info(f"Received non-JSON data from {address}: {decoded_data}")
|
||||
self.log_queue.put({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"address": address,
|
||||
"data": decoded_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Data handling error: {e}")
|
||||
|
||||
# Rest of the class remains the same...
|
||||
def _get_log_file(self):
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
if date != self.current_date or self.log_file is None:
|
||||
if self.log_file:
|
||||
self.log_file.close()
|
||||
self.current_date = date
|
||||
filename = os.path.join(self.working_directory, f"log{date}.jsonl")
|
||||
self.log_file = open(filename, 'a')
|
||||
return self.log_file
|
||||
|
||||
def _log_writer(self):
|
||||
while not self.stop_event.is_set() or not self.log_queue.empty():
|
||||
try:
|
||||
data = self.log_queue.get(timeout=1)
|
||||
log_file = self._get_log_file()
|
||||
json.dump(data, log_file)
|
||||
log_file.write('\n')
|
||||
log_file.flush()
|
||||
except Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error writing to log file: {e}")
|
||||
|
||||
def _handle_tcp_client(self, client_socket, address):
|
||||
self.logger.info(f"TCP connection established from {address}")
|
||||
try:
|
||||
with client_socket:
|
||||
while True:
|
||||
data = client_socket.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
self._handle_data(data, address)
|
||||
except Exception as e:
|
||||
self.logger.error(f"TCP client error: {e}")
|
||||
self.logger.info(f"TCP connection closed from {address}")
|
||||
|
||||
def _handle_udp_client(self, data, address):
|
||||
try:
|
||||
self._handle_data(data, address)
|
||||
except Exception as e:
|
||||
self.logger.error(f"UDP client error: {e}")
|
||||
|
||||
def _start_tcp_server(self):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_socket:
|
||||
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
tcp_socket.bind((self.host, self.port))
|
||||
tcp_socket.listen(5)
|
||||
self.logger.debug(f"TCP server running on {self.host}:{self.port}")
|
||||
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
client_socket, address = tcp_socket.accept()
|
||||
threading.Thread(target=self._handle_tcp_client,
|
||||
args=(client_socket, address),
|
||||
daemon=True).start()
|
||||
except Exception as e:
|
||||
self.logger.error(f"TCP server error: {e}")
|
||||
|
||||
def _start_udp_server(self):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket:
|
||||
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)
|
||||
udp_socket.bind((self.host, self.port))
|
||||
self.logger.debug(f"UDP server running on {self.host}:{self.port}")
|
||||
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
data, address = udp_socket.recvfrom(65535)
|
||||
self._handle_udp_client(data, address)
|
||||
except Exception as e:
|
||||
self.logger.error(f"UDP server error: {e}")
|
||||
|
||||
def start(self):
|
||||
self.logger.info("Starting MultiProtocolServer...")
|
||||
threading.Thread(target=self._log_writer, daemon=True).start()
|
||||
tcp_thread = threading.Thread(target=self._start_tcp_server, daemon=True)
|
||||
udp_thread = threading.Thread(target=self._start_udp_server, daemon=True)
|
||||
tcp_thread.start()
|
||||
udp_thread.start()
|
||||
|
||||
try:
|
||||
tcp_thread.join()
|
||||
udp_thread.join()
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
self.logger.info("Stopping Logging Server...")
|
||||
self.stop_event.set()
|
||||
if self.log_file:
|
||||
self.log_file.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Logging Server")
|
||||
parser.add_argument("-p", "--port", type=int, default=8080,
|
||||
help="Port to listen on")
|
||||
parser.add_argument("-w", "--working-directory", type=str,
|
||||
default="./logs", help="Directory to store log files")
|
||||
args = parser.parse_args()
|
||||
|
||||
server = MultiProtocolServer("0.0.0.0", args.port, args.working_directory)
|
||||
server.start()
|
37
docker/nginx.conf
Normal file
37
docker/nginx.conf
Normal file
|
@ -0,0 +1,37 @@
|
|||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server {
|
||||
listen 8081;
|
||||
server_name localhost;
|
||||
|
||||
access_log /var/log/access.log;
|
||||
error_log /var/log/error.log;
|
||||
|
||||
root /var/www/html;
|
||||
index index.php;
|
||||
|
||||
# Handle all requests
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string =503;
|
||||
autoindex off;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
# Block any .ht* files
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
61
docker/supervisord.conf
Normal file
61
docker/supervisord.conf
Normal file
|
@ -0,0 +1,61 @@
|
|||
[supervisord]
|
||||
logfile=/var/logd.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=10
|
||||
loglevel=info
|
||||
user=root
|
||||
pidfile=/var/run/supervisord.pid
|
||||
umask=022
|
||||
nodaemon=true
|
||||
minfds=1024
|
||||
minprocs=200
|
||||
|
||||
[program:logger]
|
||||
command=python3 /logger.py --port 5131
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
stdout_logfile=/var/log/fileserver.log
|
||||
stderr_logfile=/var/log/fileserver_error.log
|
||||
stdout_logfile_maxbytes=20MB
|
||||
stdout_logfile_backups=5
|
||||
stderr_logfile_maxbytes=20MB
|
||||
stderr_logfile_backups=5
|
||||
|
||||
[program:php-fpm]
|
||||
command=/usr/local/sbin/php-fpm --nodaemonize
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=20
|
||||
stdout_logfile=/var/log/fpm.log
|
||||
stderr_logfile=/var/log/fpm_error.log
|
||||
stdout_logfile_maxbytes=0
|
||||
stdout_logfile_backups=5
|
||||
stderr_logfile_maxbytes=0
|
||||
stderr_logfile_backups=5
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g "daemon off;" -c /etc/nginx/nginx.conf
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/var/log/nginx.log
|
||||
stderr_logfile=/var/log/nginx_error.log
|
||||
stdout_logfile_maxbytes=20MB
|
||||
stdout_logfile_backups=5
|
||||
stderr_logfile_maxbytes=20MB
|
||||
stderr_logfile_backups=5
|
||||
|
||||
[program:cron]
|
||||
command=cron -f -L 15
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=30
|
||||
stdout_logfile=/var/log/cron.log
|
||||
stderr_logfile=/var/log/cron_error.log
|
||||
stdout_logfile_maxbytes=20MB
|
||||
stdout_logfile_backups=5
|
||||
stderr_logfile_maxbytes=20MB
|
||||
stderr_logfile_backups=5
|
6
main
6
main
|
@ -2,7 +2,7 @@
|
|||
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
print('net.nosial.file_server must be run from the command line.' . PHP_EOL);
|
||||
print('net.nosial.fileserver must be run from the command line.' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,11 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
print('net.nosial.file_server failed to run, no $argv found.' . PHP_EOL);
|
||||
print('net.nosial.fileserver failed to run, no $argv found.' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
require('ncc');
|
||||
\ncc\Classes\Runtime::import('net.nosial.file_server', 'latest');
|
||||
\ncc\Classes\Runtime::import('net.nosial.fileserver', 'latest');
|
||||
exit(\FileServer\Program::main($argv));
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"assembly": {
|
||||
"name": "FileServer",
|
||||
"package": "net.nosial.file_server",
|
||||
"package": "net.nosial.fileserver",
|
||||
"description": "A simple standard HTTP file upload server",
|
||||
"version": "1.0.0",
|
||||
"uuid": "8ee504ac-b27f-4f67-9730-ab3904be916c"
|
||||
|
|
168
src/FileServer/Classes/Configuration.php
Normal file
168
src/FileServer/Classes/Configuration.php
Normal file
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes;
|
||||
|
||||
use FileServer\Classes\Configuration\CustomStorageConfiguration;
|
||||
use FileServer\Classes\Configuration\DatabaseConfiguration;
|
||||
use FileServer\Classes\Configuration\LocalStorageConfiguration;
|
||||
use FileServer\Classes\Configuration\ProxyStorageConfiguration;
|
||||
use FileServer\Classes\Configuration\ServerConfiguration;
|
||||
|
||||
class Configuration
|
||||
{
|
||||
private static ?\ConfigLib\Configuration $configuration = null;
|
||||
private static ?ServerConfiguration $serverConfiguration = null;
|
||||
private static ?LocalStorageConfiguration $localStorageConfiguration = null;
|
||||
private static ?ProxyStorageConfiguration $proxyStorageConfiguration = null;
|
||||
private static ?CustomStorageConfiguration $customStorageConfiguration = null;
|
||||
private static ?DatabaseConfiguration $databaseConfiguration = null;
|
||||
|
||||
/**
|
||||
* Initializes the default settings for the configuration and constructs the private properties
|
||||
* for the configuration class
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeConfiguration(): void
|
||||
{
|
||||
self::$configuration = new \ConfigLib\Configuration('fileserver');
|
||||
self::$configuration->setDefault('server.name', 'FileServer');
|
||||
self::$configuration->setDefault('server.storage_type', 'local');
|
||||
self::$configuration->setDefault('server.password', null);
|
||||
self::$configuration->setDefault('server.upload_password', null);
|
||||
self::$configuration->setDefault('server.read_only', false);
|
||||
self::$configuration->setDefault('server.filter_extensions', false);
|
||||
self::$configuration->setDefault('server.allowed_extensions', ['jpg', 'jpeg', 'png', 'pdf', 'docx', 'xlsx', 'txt']);
|
||||
self::$configuration->setDefault('server.max_execution_time', 3600); // 1 hour
|
||||
self::$configuration->setDefault('server.max_file_size', 1073741824); // 1GB
|
||||
self::$configuration->setDefault('server.working_path', '/etc/fileserver/tmp'); // If null, will default to a temporary directory
|
||||
|
||||
// Database Configuration
|
||||
self::$configuration->setDefault('database.driver', 'sql');
|
||||
// PDO Configuration
|
||||
self::$configuration->setDefault('database.sql.host', '127.0.0.1');
|
||||
self::$configuration->setDefault('database.sql.port', '3306');
|
||||
self::$configuration->setDefault('database.sql.username', 'root');
|
||||
self::$configuration->setDefault('database.sql.password', 'root');
|
||||
self::$configuration->setDefault('database.sql.database', 'fileserver');
|
||||
// SQLITE Configuration
|
||||
self::$configuration->setDefault('database.sqlite.path', '/etc/fileserver/database.sqlite');
|
||||
|
||||
// Local storage configuration
|
||||
self::$configuration->setDefault('local_storage.storage_directory', '/var/www/uploads');
|
||||
self::$configuration->setDefault('local_storage.max_file_size', 2147483648); // 2GB
|
||||
self::$configuration->setDefault('local_storage.max_storage_size', 536870912000); // 500GB Max storage size
|
||||
self::$configuration->setDefault('local_storage.return_content_size', true); // If True, Content-Size is returned
|
||||
|
||||
// Proxy storage configuration
|
||||
self::$configuration->setDefault('proxy_storage.endpoint', 'https://proxy.example.com/upload');
|
||||
self::$configuration->setDefault('proxy_storage.upload_password', null);
|
||||
self::$configuration->setDefault('proxy_storage.admin_password', null);
|
||||
self::$configuration->setDefault('proxy_storage.max_file_size', 2147483648); // 2GB
|
||||
self::$configuration->setDefault('proxy_storage.curl_timeout', 3600); // 1 hour
|
||||
self::$configuration->setDefault('proxy_storage.return_content_size', true); // If True, Content-Size is returnedp
|
||||
|
||||
// Custom storage configuration
|
||||
self::$configuration->setDefault('custom_storage.package', null);
|
||||
self::$configuration->setDefault('custom_storage.class', null);
|
||||
self::$configuration->setDefault('custom_storage.config', []);
|
||||
|
||||
// Save & load the configuration
|
||||
self::$configuration->save();
|
||||
self::$serverConfiguration = new ServerConfiguration(self::$configuration->getConfiguration()['server']);
|
||||
self::$localStorageConfiguration = new LocalStorageConfiguration(self::$configuration->getConfiguration()['local_storage']);
|
||||
self::$proxyStorageConfiguration = new ProxyStorageConfiguration(self::$configuration->getConfiguration()['proxy_storage']);
|
||||
self::$customStorageConfiguration = new CustomStorageConfiguration(self::$configuration->getConfiguration()['custom_storage']);
|
||||
self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main ConfigurationLib instance for this instance
|
||||
*
|
||||
* @return \ConfigLib\Configuration The ConfigurationLib object
|
||||
*/
|
||||
public static function getConfigurationLib(): \ConfigLib\Configuration
|
||||
{
|
||||
if(self::$configuration === null)
|
||||
{
|
||||
self::initializeConfiguration();
|
||||
}
|
||||
|
||||
return self::$configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the server configuration object
|
||||
*
|
||||
* @return ServerConfiguration The server configuration object
|
||||
*/
|
||||
public static function getServerConfiguration(): ServerConfiguration
|
||||
{
|
||||
if(self::$serverConfiguration === null)
|
||||
{
|
||||
self::initializeConfiguration();
|
||||
}
|
||||
|
||||
return self::$serverConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local storage configuration object
|
||||
*
|
||||
* @return LocalStorageConfiguration The local storage configuration object
|
||||
*/
|
||||
public static function getLocalStorageConfiguration(): LocalStorageConfiguration
|
||||
{
|
||||
if(self::$localStorageConfiguration === null)
|
||||
{
|
||||
self::initializeConfiguration();
|
||||
}
|
||||
|
||||
return self::$localStorageConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proxy storage configuration object
|
||||
*
|
||||
* @return ProxyStorageConfiguration The proxy storage configuration object
|
||||
*/
|
||||
public static function getProxyStorageConfiguration(): ProxyStorageConfiguration
|
||||
{
|
||||
if(self::$proxyStorageConfiguration === null)
|
||||
{
|
||||
self::initializeConfiguration();
|
||||
}
|
||||
|
||||
return self::$proxyStorageConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom storage configuration object
|
||||
*
|
||||
* @return CustomStorageConfiguration The custom storage configuration object
|
||||
*/
|
||||
public static function getCustomStorageConfiguration(): CustomStorageConfiguration
|
||||
{
|
||||
if(self::$customStorageConfiguration === null)
|
||||
{
|
||||
self::initializeConfiguration();
|
||||
}
|
||||
|
||||
return self::$customStorageConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the database configuration object
|
||||
*
|
||||
* @return DatabaseConfiguration The database configuration object
|
||||
*/
|
||||
public static function getDatabaseConfiguration(): DatabaseConfiguration
|
||||
{
|
||||
if(self::$databaseConfiguration === null)
|
||||
{
|
||||
self::initializeConfiguration();
|
||||
}
|
||||
|
||||
return self::$databaseConfiguration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
class CustomStorageConfiguration
|
||||
{
|
||||
private ?string $package;
|
||||
private ?string $class;
|
||||
private ?array $config;
|
||||
|
||||
/**
|
||||
* CustomStorageConfiguration constructor.
|
||||
*
|
||||
* @param array $data The configuration data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->package = $data['package'];
|
||||
$this->class = $data['class'];
|
||||
$this->config = $data['config'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of the custom storage
|
||||
*
|
||||
* @return string|null The package name
|
||||
*/
|
||||
public function getPackage(): ?string
|
||||
{
|
||||
return $this->package;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class name of the custom storage
|
||||
*
|
||||
* @return string|null The class name
|
||||
*/
|
||||
public function getClass(): ?string
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options for the custom storage
|
||||
*
|
||||
* @return array|null The options
|
||||
*/
|
||||
public function getConfig(): ?array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
use FileServer\Enums\DatabaseDriverType;
|
||||
|
||||
class DatabaseConfiguration
|
||||
{
|
||||
private DatabaseDriverType $driver;
|
||||
private SqlDatabaseConfiguration $sqlDatabaseConfiguration;
|
||||
private SqliteDatabaseConfiguration $sqliteDatabaseConfiguration;
|
||||
|
||||
/**
|
||||
* DatabaseConfiguration Public Constructor
|
||||
*
|
||||
* @param array $data The array data to construct with
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->driver = DatabaseDriverType::from(mb_strtoupper($data['driver']));
|
||||
$this->sqlDatabaseConfiguration = new SqlDatabaseConfiguration($data['sql']);
|
||||
$this->sqliteDatabaseConfiguration = new SqliteDatabaseConfiguration($data['sqlite']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the driver type used for the database configuration
|
||||
*
|
||||
* @return DatabaseDriverType The driver type used
|
||||
*/
|
||||
public function getDriver(): DatabaseDriverType
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQL configuration
|
||||
*
|
||||
* @return SqlDatabaseConfiguration The SQL Configuration
|
||||
*/
|
||||
public function getSqlDatabaseConfiguration(): SqlDatabaseConfiguration
|
||||
{
|
||||
return $this->sqlDatabaseConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQLITE configuration
|
||||
*
|
||||
* @return SqliteDatabaseConfiguration The SQLITE Configuration
|
||||
*/
|
||||
public function getSqliteDatabaseConfiguration(): SqliteDatabaseConfiguration
|
||||
{
|
||||
return $this->sqliteDatabaseConfiguration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
class LocalStorageConfiguration
|
||||
{
|
||||
private string $storageDirectory;
|
||||
private int $maxFileSize;
|
||||
private int $maxStorageSize;
|
||||
private bool $returnContentSize;
|
||||
|
||||
/**
|
||||
* Constructor for LocalStorageConfiguration.
|
||||
*
|
||||
* @param array $data The configuration data containing 'path' and 'max_size'.
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->storageDirectory = $data['storage_directory'];
|
||||
$this->maxFileSize = $data['max_file_size'];
|
||||
$this->maxStorageSize = $data['max_storage_size'];
|
||||
$this->returnContentSize = $data['return_content_size'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the local storage directory.
|
||||
*
|
||||
* @return string The path to the local storage directory.
|
||||
*/
|
||||
public function getStorageDirectory(): string
|
||||
{
|
||||
return $this->storageDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum size of a file upload
|
||||
*
|
||||
* @return int The maximum size of a file that can be uploaded
|
||||
*/
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return $this->maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum storage size of the local storage in byets
|
||||
*
|
||||
* @return int The maximum size of the local storage
|
||||
*/
|
||||
public function getMaxStorageSize(): int
|
||||
{
|
||||
return $this->maxStorageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the content size should be returned.
|
||||
*
|
||||
* @return bool True if the content size should be returned, false otherwise.
|
||||
*/
|
||||
public function shouldReturnContentSize(): bool
|
||||
{
|
||||
return $this->returnContentSize;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
class ProxyStorageConfiguration
|
||||
{
|
||||
private string $endpoint;
|
||||
private ?string $uploadPassword;
|
||||
private ?string $adminPassword;
|
||||
private int $maxFileSize;
|
||||
private int $curlTimeout;
|
||||
|
||||
/**
|
||||
* Constructor to initialize the ProxyConfiguration object with data from the configuration array.
|
||||
*
|
||||
* @param array $data The configuration data array containing keys 'endpoint', 'password', 'stream_chunk_size', and 'curl_timeout'.
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->endpoint = (string)$data['endpoint'];
|
||||
$this->uploadPassword = (string)$data['upload_password'] ?? null;
|
||||
$this->adminPassword = (string)$data['admin_password'] ?? null;
|
||||
$this->maxFileSize = (int)$data['max_file_size'];
|
||||
$this->curlTimeout = (int)$data['curl_timeout'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the endpoint of the proxy server to upload to
|
||||
*
|
||||
* @return string The endpoint of the proxy server
|
||||
*/
|
||||
public function getEndpoint(): string
|
||||
{
|
||||
return $this->endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional. Returns the upload password if one is required to upload a file to the proxy server
|
||||
*
|
||||
* @return string|null The upload password, null if none is set.
|
||||
*/
|
||||
public function getUploadPassword(): ?string
|
||||
{
|
||||
return $this->uploadPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional. Returns the admin password if one is required to manage the proxy server
|
||||
*
|
||||
* @return string|null The admin password, null if none is set
|
||||
*/
|
||||
public function getAdminPassword(): ?string
|
||||
{
|
||||
return $this->adminPassword;
|
||||
}
|
||||
|
||||
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return $this->maxFileSize;
|
||||
}
|
||||
|
||||
public function getCurlTimeout(): int
|
||||
{
|
||||
return $this->curlTimeout;
|
||||
}
|
||||
}
|
144
src/FileServer/Classes/Configuration/ServerConfiguration.php
Normal file
144
src/FileServer/Classes/Configuration/ServerConfiguration.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
use FileServer\Enums\StorageType;
|
||||
|
||||
class ServerConfiguration
|
||||
{
|
||||
private string $name;
|
||||
private StorageType $storageType;
|
||||
private ?string $uploadPassword;
|
||||
private ?string $password;
|
||||
private bool $readOnly;
|
||||
private bool $filterExtensions;
|
||||
private array $allowedExtensions;
|
||||
private int $maxExecutionTime;
|
||||
private int $maxFileSize;
|
||||
private ?string $workingPath;
|
||||
|
||||
/**
|
||||
* ServerConfiguration constructor.
|
||||
*
|
||||
* @param array $data The configuration data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->name = $data['name'];
|
||||
$this->storageType = StorageType::from(mb_strtoupper($data['storage_type']));
|
||||
$this->password = $data['password'];
|
||||
$this->uploadPassword = $data['upload_password'];
|
||||
$this->readOnly = (bool)$data['read_only'];
|
||||
$this->filterExtensions = (bool)$data['filter_extensions'];
|
||||
$this->allowedExtensions = $data['allowed_extensions'];
|
||||
$this->maxExecutionTime = (int)$data['max_execution_time'];
|
||||
$this->maxFileSize = (int)$data['max_file_size'];
|
||||
$this->workingPath = $data['working_path'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the server
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type of the server.
|
||||
*
|
||||
* @return StorageType The storage type
|
||||
*/
|
||||
public function getStorageType(): StorageType
|
||||
{
|
||||
return $this->storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password for the server.
|
||||
*
|
||||
* @return string|null The password or null if not set
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the upload password or null if not set
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUploadPassword(): ?string
|
||||
{
|
||||
return $this->uploadPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server is in read-only mode.
|
||||
*
|
||||
* @return bool True if read-only, false otherwise
|
||||
*/
|
||||
public function isReadOnly(): bool
|
||||
{
|
||||
return $this->readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server is filtering file extensions.
|
||||
*
|
||||
* @return bool True if filtering extensions, false otherwise
|
||||
*/
|
||||
public function isFilteringExtensions(): bool
|
||||
{
|
||||
return $this->filterExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed file extensions.
|
||||
*
|
||||
* @return array The allowed file extensions
|
||||
*/
|
||||
public function getAllowedExtensions(): array
|
||||
{
|
||||
return $this->allowedExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum execution time in seconds.
|
||||
*
|
||||
* @return int The maximum execution time
|
||||
*/
|
||||
public function getMaxExecutionTime(): int
|
||||
{
|
||||
return $this->maxExecutionTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum file size in bytes.
|
||||
*
|
||||
* @return int The maximum file size
|
||||
*/
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return $this->maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the working path for working with temporary files, if null the method will return the system's
|
||||
* temporary directory instead.
|
||||
*
|
||||
* @return string The working path
|
||||
*/
|
||||
public function getWorkingPath(): string
|
||||
{
|
||||
if($this->workingPath === null)
|
||||
{
|
||||
return sys_get_temp_dir();
|
||||
}
|
||||
|
||||
return $this->workingPath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
class SqlDatabaseConfiguration
|
||||
{
|
||||
private string $host;
|
||||
private int $port;
|
||||
private string $username;
|
||||
private string $password;
|
||||
private string $database;
|
||||
|
||||
/**
|
||||
* Constructs the SqlDatabaseConfiguration object
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->host = $data['host'];
|
||||
$this->port = (int)$data['port'];
|
||||
$this->username = $data['username'];
|
||||
$this->password = $data['password'];
|
||||
$this->database = $data['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the host of the database
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the port of the database
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username of the database
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password of the database
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the database name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabase(): string
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full DSN connection string of the database
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDsn(): string
|
||||
{
|
||||
return sprintf('mysql:host=%s;port=%d;dbname=%s', $this->host, $this->port, $this->database);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Configuration;
|
||||
|
||||
class SqliteDatabaseConfiguration
|
||||
{
|
||||
private string $path;
|
||||
|
||||
/**
|
||||
* Constructs the SqliteDatabaseConfiguration object
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->path = $data['path'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of the SQLite database
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
}
|
167
src/FileServer/Classes/Database.php
Normal file
167
src/FileServer/Classes/Database.php
Normal file
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes;
|
||||
|
||||
use FileServer\Enums\DatabaseDriverType;
|
||||
use FileServer\Exceptions\DatabaseException;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
|
||||
class Database
|
||||
{
|
||||
private static ?PDO $connection;
|
||||
private static bool $initializationChecked = false;
|
||||
|
||||
/**
|
||||
* Returns the PDO connection to the database.
|
||||
*
|
||||
* @return PDO The PDO connection object.
|
||||
* @throws DatabaseException If an error occurs while creating the connection.
|
||||
*/
|
||||
public static function getConnection(): PDO
|
||||
{
|
||||
if(self::$connection === null)
|
||||
{
|
||||
self::createConnection();
|
||||
}
|
||||
|
||||
return self::$connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
private static function createConnection(): void
|
||||
{
|
||||
if(Configuration::getDatabaseConfiguration()->getDriver() === DatabaseDriverType::SQL)
|
||||
{
|
||||
self::$connection = new PDO(
|
||||
dsn: Configuration::getDatabaseConfiguration()->getSqlDatabaseConfiguration()->getDsn(),
|
||||
username: Configuration::getDatabaseConfiguration()->getSqlDatabaseConfiguration()->getUsername(),
|
||||
password: Configuration::getDatabaseConfiguration()->getSqlDatabaseConfiguration()->getPassword(),
|
||||
);
|
||||
|
||||
// Initialization must be done after connection
|
||||
if(!self::$initializationChecked)
|
||||
{
|
||||
self::initializeSqlDatabase();
|
||||
self::$initializationChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(Configuration::getDatabaseConfiguration()->getDriver() === DatabaseDriverType::SQLITE)
|
||||
{
|
||||
// Initialize the SQLite database if it doesn't exist, must be done before connection
|
||||
if(!self::$initializationChecked)
|
||||
{
|
||||
self::initializeSqliteSDatabase();
|
||||
self::$initializationChecked = true;
|
||||
}
|
||||
|
||||
self::$connection = new PDO(
|
||||
dsn: sprintf('sqlite:%s', Configuration::getDatabaseConfiguration()->getSqliteDatabaseConfiguration()->getPath()),
|
||||
);
|
||||
}
|
||||
|
||||
// Set the error mode to exception and turn off emulated prepares
|
||||
self::$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SQLite database by copying the initial database file if it doesn't exist.
|
||||
*
|
||||
* @throws DatabaseException If an error occurs while copying the database file.
|
||||
*/
|
||||
private static function initializeSqliteSDatabase(): void
|
||||
{
|
||||
if(file_exists(Configuration::getDatabaseConfiguration()->getSqliteDatabaseConfiguration()->getPath()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!copy(
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'database.sqlite',
|
||||
Configuration::getDatabaseConfiguration()->getSqliteDatabaseConfiguration()->getPath()
|
||||
))
|
||||
{
|
||||
throw new DatabaseException(sprintf('Failed to copy SQLite database file to %s', Configuration::getDatabaseConfiguration()->getSqliteDatabaseConfiguration()->getPath()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SQL database by creating the necessary tables if they do not exist.
|
||||
*
|
||||
* @throws DatabaseException If an error occurs while creating the tables.
|
||||
*/
|
||||
|
||||
private static function initializeSqlDatabase(): void
|
||||
{
|
||||
// Check
|
||||
|
||||
if(!self::sqlTableExists('uploads'))
|
||||
{
|
||||
$uploadsTable = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'uploads.sql';
|
||||
if(!file_exists($uploadsTable) || !is_readable($uploadsTable))
|
||||
{
|
||||
throw new RuntimeException(sprintf('Resource file %s not found', $uploadsTable));
|
||||
}
|
||||
|
||||
// Safely load the SQL file to create the table
|
||||
try
|
||||
{
|
||||
self::$connection->exec(file_get_contents($uploadsTable));
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Error creating uploads table: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if(!self::sqlTableExists('statistics'))
|
||||
{
|
||||
$statisticsTable = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'statistics.sql';
|
||||
if(!file_exists($statisticsTable) || !is_readable($statisticsTable))
|
||||
{
|
||||
throw new RuntimeException(sprintf('Resource file %s not found', $statisticsTable));
|
||||
}
|
||||
|
||||
// Safely load the SQL file to create the table
|
||||
try
|
||||
{
|
||||
self::$connection->exec(file_get_contents($statisticsTable));
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Error creating statistics table: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified table exists in the database.
|
||||
*
|
||||
* @param string $tableName The name of the table to check.
|
||||
* @return bool True if the table exists, false otherwise.
|
||||
* @throws DatabaseException If an error occurs while checking the table existence.
|
||||
*/
|
||||
private static function sqlTableExists(string $tableName): bool
|
||||
{
|
||||
try
|
||||
{
|
||||
self::$connection->query("SELECT 1 FROM $tableName LIMIT 1");
|
||||
return true;
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
if($e->getCode() === '42S02') // Table not found
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new DatabaseException('Error checking table existence: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
345
src/FileServer/Classes/Handlers/LocalHandler.php
Normal file
345
src/FileServer/Classes/Handlers/LocalHandler.php
Normal file
|
@ -0,0 +1,345 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Handlers;
|
||||
|
||||
use Exception;
|
||||
use FileServer\Classes\Configuration;
|
||||
use FileServer\Classes\IndexStorageManager;
|
||||
use FileServer\Exceptions\ServerException;
|
||||
use FileServer\Exceptions\UploadException;
|
||||
use FileServer\FileServer;
|
||||
use FileServer\Interfaces\UploadHandlerInterface;
|
||||
use FileServer\Objects\IndexStorageRecord;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class LocalHandler implements UploadHandlerInterface
|
||||
{
|
||||
// Configurable constants
|
||||
private const int MAX_FILENAME_LENGTH = 255;
|
||||
private const string FILENAME_PATTERN = '/^[a-zA-Z0-9_\-. ]+$/u'; // Safe characters only
|
||||
private const int BUFFER_SIZE = 8192; // 8 KB buffer size
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function handleUpload(string $uuid): array
|
||||
{
|
||||
// Ensure the request is POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'PUT')
|
||||
{
|
||||
FileServer::basicResponse(405, 'Method Not Allowed');
|
||||
exit;
|
||||
}
|
||||
|
||||
$isMultipart = str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'multipart/form-data');
|
||||
$tmpName = null;
|
||||
$fileSize = 0;
|
||||
|
||||
if ($isMultipart)
|
||||
{
|
||||
// Check for multiple files
|
||||
if (count($_FILES) > 1)
|
||||
{
|
||||
throw new ServerException('Multiple files not allowed');
|
||||
}
|
||||
|
||||
$fileKey = key($_FILES);
|
||||
if (!$fileKey)
|
||||
{
|
||||
throw new ServerException('No file provided');
|
||||
}
|
||||
|
||||
$file = $_FILES[$fileKey];
|
||||
|
||||
// Handle array uploads
|
||||
if (is_array($file['name']))
|
||||
{
|
||||
if (count($file['name']) > 1)
|
||||
{
|
||||
throw new ServerException('Multiple files not allowed');
|
||||
}
|
||||
|
||||
$fileName = $file['name'][0];
|
||||
$tmpName = $file['tmp_name'][0];
|
||||
$uploadError = $file['error'][0];
|
||||
}
|
||||
else
|
||||
{
|
||||
$fileName = $file['name'];
|
||||
$tmpName = $file['tmp_name'];
|
||||
$uploadError = $file['error'];
|
||||
}
|
||||
|
||||
if ($uploadError !== UPLOAD_ERR_OK)
|
||||
{
|
||||
throw new ServerException("Upload error: " . $uploadError);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Raw upload: get filename from headers
|
||||
// Try X-Filename header
|
||||
$fileName = self::getHeaderValue('X-Filename');
|
||||
|
||||
// If not found, check Content-Disposition
|
||||
if (!$fileName)
|
||||
{
|
||||
$contentDisposition = self::getHeaderValue('Content-Disposition');
|
||||
if ($contentDisposition)
|
||||
{
|
||||
preg_match('/filename="?([^"\r\n]+)"?/', $contentDisposition, $matches);
|
||||
if (!empty($matches[1]))
|
||||
{
|
||||
$fileName = $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate filename length
|
||||
if (!is_null($fileName) && mb_strlen($fileName) > self::MAX_FILENAME_LENGTH)
|
||||
{
|
||||
throw new ServerException('Filename exceeds ' . self::MAX_FILENAME_LENGTH . ' characters');
|
||||
}
|
||||
|
||||
// Sanitize filename: remove path info
|
||||
if(!is_null($fileName))
|
||||
{
|
||||
$fileName = basename($fileName);
|
||||
|
||||
// Validate filename characters
|
||||
if (!preg_match(self::FILENAME_PATTERN, $fileName))
|
||||
{
|
||||
throw new ServerException('Invalid filename characters');
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare storage path
|
||||
$baseStoragePath = Configuration::getLocalStorageConfiguration()->getStorageDirectory();
|
||||
if (!is_dir($baseStoragePath) && !mkdir($baseStoragePath, 0755, true))
|
||||
{
|
||||
throw new UploadException('Failed to create storage directory');
|
||||
}
|
||||
|
||||
if(is_null($fileName))
|
||||
{
|
||||
$targetPath = $baseStoragePath . DIRECTORY_SEPARATOR . $uuid;
|
||||
}
|
||||
else
|
||||
{
|
||||
$targetPath = $baseStoragePath . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
|
||||
// Initialize SHA-256 hashing context
|
||||
if ($isMultipart)
|
||||
{
|
||||
if (!is_uploaded_file($tmpName))
|
||||
{
|
||||
throw new UploadException("Invalid uploaded file");
|
||||
}
|
||||
|
||||
// Open file and stream into hash context
|
||||
$inputStream = fopen($tmpName, 'rb');
|
||||
if (!$inputStream)
|
||||
{
|
||||
throw new UploadException("Failed to open temporary file");
|
||||
}
|
||||
|
||||
$fileHandle = fopen($targetPath, 'wb');
|
||||
if (!$fileHandle)
|
||||
{
|
||||
@fclose($inputStream);
|
||||
@unlink($tmpName);
|
||||
throw new UploadException("Failed to open target file for writing");
|
||||
}
|
||||
|
||||
while (!feof($inputStream))
|
||||
{
|
||||
if($fileSize > Configuration::getLocalStorageConfiguration()->getMaxFileSize())
|
||||
{
|
||||
fclose($inputStream);
|
||||
fclose($fileHandle);
|
||||
unlink($targetPath);
|
||||
|
||||
throw new UploadException(sprintf('File size exceeds maximum limit of %d bytes', Configuration::getLocalStorageConfiguration()->getMaxFileSize()));
|
||||
}
|
||||
|
||||
$buffer = fread($inputStream, self::BUFFER_SIZE);
|
||||
$bytesRead = strlen($buffer);
|
||||
|
||||
if ($bytesRead === 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fwrite($fileHandle, $buffer);
|
||||
$fileSize += $bytesRead;
|
||||
}
|
||||
|
||||
fclose($inputStream);
|
||||
fclose($fileHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stream raw upload to file
|
||||
$inputStream = fopen('php://input', 'rb');
|
||||
if (!$inputStream)
|
||||
{
|
||||
throw new UploadException('Failed to read input stream');
|
||||
}
|
||||
|
||||
$fileHandle = fopen($targetPath, 'wb');
|
||||
if (!$fileHandle)
|
||||
{
|
||||
fclose($inputStream);
|
||||
throw new UploadException('Failed to open target file for writing');
|
||||
}
|
||||
|
||||
while (!feof($inputStream))
|
||||
{
|
||||
if($fileSize > Configuration::getLocalStorageConfiguration()->getMaxFileSize())
|
||||
{
|
||||
fclose($inputStream);
|
||||
fclose($fileHandle);
|
||||
unlink($targetPath);
|
||||
|
||||
throw new UploadException(sprintf('File size exceeds maximum limit of %d bytes', Configuration::getLocalStorageConfiguration()->getMaxFileSize()));
|
||||
}
|
||||
|
||||
$buffer = fread($inputStream, self::BUFFER_SIZE);
|
||||
$bytesRead = strlen($buffer);
|
||||
|
||||
if ($bytesRead === 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fwrite($fileHandle, $buffer);
|
||||
$fileSize += $bytesRead;
|
||||
}
|
||||
|
||||
fclose($inputStream);
|
||||
fclose($fileHandle);
|
||||
}
|
||||
|
||||
// Update storage record
|
||||
try
|
||||
{
|
||||
if(!is_null($fileName))
|
||||
{
|
||||
IndexStorageManager::updateUploadName($uuid, $fileName);
|
||||
}
|
||||
|
||||
IndexStorageManager::updateUploadSize($uuid, $fileSize);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
@unlink($targetPath); // Attempt to delete file
|
||||
throw new UploadException('Failed to update storage record: ' . $e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up temporary file if it exists
|
||||
if ($isMultipart && file_exists($tmpName))
|
||||
{
|
||||
@unlink($tmpName);
|
||||
}
|
||||
}
|
||||
|
||||
// Success, return the pointers for later
|
||||
return [
|
||||
'filename' => $fileName,
|
||||
'filepath' => $targetPath,
|
||||
'size' => $fileSize,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to safely retrieve HTTP headers across server environments
|
||||
*
|
||||
* @param string $headerName The name of the header to retrieve
|
||||
* @return string|null The value of the header or null if not set
|
||||
*/
|
||||
private static function getHeaderValue(string $headerName): ?string
|
||||
{
|
||||
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $headerName));
|
||||
return $_SERVER[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total size of all files in the local storage directory
|
||||
*
|
||||
* @return int The total size of all files in the local storage directory in bytes
|
||||
*/
|
||||
private static function calculateTotalStorageSize(): int
|
||||
{
|
||||
$storageDirectory = Configuration::getLocalStorageConfiguration()->getStorageDirectory();
|
||||
$totalSize = 0;
|
||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($storageDirectory));
|
||||
foreach ($files as $file)
|
||||
{
|
||||
if ($file->isFile())
|
||||
{
|
||||
$totalSize += $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function handleDownload(IndexStorageRecord $record): void
|
||||
{
|
||||
$filePath = $record->getPointers()['filepath'];
|
||||
if (!file_exists($filePath))
|
||||
{
|
||||
FileServer::basicResponse(404, 'File not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set appropriate headers
|
||||
header('Content-Type: application/octet-stream');
|
||||
if(Configuration::getLocalStorageConfiguration()->shouldReturnContentSize())
|
||||
{
|
||||
header('Content-Length: ' . $record->getSize());
|
||||
}
|
||||
|
||||
header("Content-Disposition: attachment; filename=\"" . rawurlencode($record->getName()) . "\"");
|
||||
|
||||
// Stream file in chunks
|
||||
$handle = fopen($filePath, 'rb');
|
||||
while (!feof($handle))
|
||||
{
|
||||
echo fread($handle, 8192);
|
||||
ob_flush();
|
||||
flush();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function handleDelete(IndexStorageRecord $record): void
|
||||
{
|
||||
$filePath = $record->getPointers()['filepath'];
|
||||
|
||||
if (file_exists($filePath) && !@unlink($filePath))
|
||||
{
|
||||
throw new ServerException("Failed to delete file: " . $filePath);
|
||||
}
|
||||
|
||||
// Clean up UUID directory if empty
|
||||
$dirPath = dirname($filePath);
|
||||
if (is_dir($dirPath) && count(scandir($dirPath)) === 2)
|
||||
{
|
||||
rmdir($dirPath);
|
||||
}
|
||||
}
|
||||
}
|
422
src/FileServer/Classes/Handlers/ProxyHandler.php
Normal file
422
src/FileServer/Classes/Handlers/ProxyHandler.php
Normal file
|
@ -0,0 +1,422 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes\Handlers;
|
||||
|
||||
use Exception;
|
||||
use FileServer\Classes\Configuration;
|
||||
use FileServer\Classes\IndexStorageManager;
|
||||
use FileServer\Exceptions\ServerException;
|
||||
use FileServer\Exceptions\UploadException;
|
||||
use FileServer\FileServer;
|
||||
use FileServer\Interfaces\UploadHandlerInterface;
|
||||
use FileServer\Objects\IndexStorageRecord;
|
||||
use RuntimeException;
|
||||
|
||||
class ProxyHandler implements UploadHandlerInterface
|
||||
{
|
||||
public const int MAX_FILENAME_LENGTH = 255;
|
||||
public const string FILENAME_PATTERN = '/^[a-zA-Z0-9_\-.]+$/';
|
||||
public const string UUID_PATTERN = '/^[a-f0-9]{8}-[a-f0-9]{4}-[4][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function handleUpload(string $uuid): array
|
||||
{
|
||||
// Validate UUID format
|
||||
if (!preg_match(self::UUID_PATTERN, $uuid))
|
||||
{
|
||||
FileServer::basicResponse(400, 'Invalid UUID format');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate request method
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'PUT')
|
||||
{
|
||||
throw new UploadException('Method not allowed', 400);
|
||||
}
|
||||
|
||||
// Detect upload type
|
||||
$isMultipart = str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'multipart/form-data');
|
||||
$tmpName = null;
|
||||
|
||||
// Handle multipart uploads
|
||||
if ($isMultipart)
|
||||
{
|
||||
if (count($_FILES) > 1)
|
||||
{
|
||||
throw new ServerException('Multiple files not allowed');
|
||||
}
|
||||
|
||||
$fileKey = key($_FILES);
|
||||
if (!$fileKey)
|
||||
{
|
||||
throw new ServerException('No file provided');
|
||||
}
|
||||
|
||||
$file = $_FILES[$fileKey];
|
||||
|
||||
// Handle array format uploads
|
||||
if (is_array($file['name']))
|
||||
{
|
||||
if (count($file['name']) > 1)
|
||||
{
|
||||
throw new ServerException('Multiple files not allowed');
|
||||
}
|
||||
|
||||
$fileName = $file['name'][0];
|
||||
$tmpName = $file['tmp_name'][0];
|
||||
$uploadError = $file['error'][0];
|
||||
}
|
||||
else
|
||||
{
|
||||
$fileName = $file['name'];
|
||||
$tmpName = $file['tmp_name'];
|
||||
$uploadError = $file['error'];
|
||||
}
|
||||
|
||||
if ($uploadError !== UPLOAD_ERR_OK)
|
||||
{
|
||||
throw new ServerException("Upload error: " . $uploadError);
|
||||
}
|
||||
|
||||
if (!is_uploaded_file($tmpName))
|
||||
{
|
||||
throw new ServerException('Invalid uploaded file');
|
||||
}
|
||||
}
|
||||
// Handle raw uploads
|
||||
else
|
||||
{
|
||||
// Check Content-Length early for raw uploads
|
||||
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? null;
|
||||
if ($contentLength !== null && $contentLength > Configuration::getProxyStorageConfiguration()->getMaxFileSize())
|
||||
{
|
||||
throw new UploadException(sprintf('File size exceeds maximum limit of %d bytes', Configuration::getProxyStorageConfiguration()->getMaxFileSize()));
|
||||
}
|
||||
|
||||
$fileName = self::getHeaderValue('X-Filename');
|
||||
if (!$fileName)
|
||||
{
|
||||
$contentDisposition = self::getHeaderValue('Content-Disposition');
|
||||
if ($contentDisposition)
|
||||
{
|
||||
preg_match('/filename="?([^"\r\n]+)"?/', $contentDisposition, $matches);
|
||||
if (!empty($matches[1]))
|
||||
{
|
||||
$fileName = $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate filename
|
||||
if ($fileName !== null)
|
||||
{
|
||||
if (mb_strlen($fileName) > self::MAX_FILENAME_LENGTH)
|
||||
{
|
||||
throw new ServerException("Filename exceeds " . self::MAX_FILENAME_LENGTH . " characters");
|
||||
}
|
||||
|
||||
$fileName = basename($fileName);
|
||||
if (!preg_match(self::FILENAME_PATTERN, $fileName))
|
||||
{
|
||||
throw new ServerException("Invalid filename characters");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize streams and tracking
|
||||
$inputStream = $isMultipart ? fopen($tmpName, 'rb') : fopen('php://input', 'rb');
|
||||
if (!$inputStream)
|
||||
{
|
||||
throw new UploadException("Failed to open input stream");
|
||||
}
|
||||
|
||||
// Initialize hash context
|
||||
$hashContext = hash_init('sha256');
|
||||
$fileSize = 0;
|
||||
$maxSize = Configuration::getProxyStorageConfiguration()->getMaxFileSize();
|
||||
$sizeExceeded = false;
|
||||
|
||||
// Configure cURL
|
||||
$remoteUrl = Configuration::getProxyStorageConfiguration()->getEndpoint();
|
||||
if (!$remoteUrl)
|
||||
{
|
||||
fclose($inputStream);
|
||||
throw new UploadException("Remote endpoint not configured");
|
||||
}
|
||||
|
||||
$uploadPassword = Configuration::getProxyStorageConfiguration()->getUploadPassword();
|
||||
|
||||
// Setup cURL options
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $remoteUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_UPLOAD => true,
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_READFUNCTION => function ($ch, $fd, $length) use ($inputStream, $hashContext, &$fileSize, $maxSize, &$sizeExceeded)
|
||||
{
|
||||
if (!is_resource($inputStream) || feof($inputStream))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$buffer = fread($inputStream, $length);
|
||||
if ($buffer === false)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$bytesRead = strlen($buffer);
|
||||
if ($bytesRead === 0)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$fileSize += $bytesRead;
|
||||
|
||||
if ($fileSize > $maxSize)
|
||||
{
|
||||
$sizeExceeded = true;
|
||||
fclose($inputStream);
|
||||
return '';
|
||||
}
|
||||
|
||||
hash_update($hashContext, $buffer);
|
||||
return $buffer;
|
||||
},
|
||||
CURLOPT_HTTPHEADER => self::buildRemoteHeaders($fileName, $uploadPassword),
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 300
|
||||
]);
|
||||
|
||||
// Execute upload
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
// Parse the response header for X-UUID
|
||||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$responseHeaders = substr($response, 0, $headerSize);
|
||||
$responseUuid = null;
|
||||
|
||||
foreach (explode("\r\n", $responseHeaders) as $header)
|
||||
{
|
||||
if (str_starts_with(strtolower($header), 'x-uuid:'))
|
||||
{
|
||||
$responseUuid = trim(substr($header, strlen('X-UUID:')));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
// Close input stream
|
||||
if (is_resource($inputStream))
|
||||
{
|
||||
fclose($inputStream);
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if ($curlError)
|
||||
{
|
||||
throw new UploadException("cURL error during upload: " . $curlError);
|
||||
}
|
||||
|
||||
if ($sizeExceeded)
|
||||
{
|
||||
throw new UploadException(sprintf('File size exceeds maximum limit of %d bytes', $maxSize));
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300)
|
||||
{
|
||||
throw new UploadException("Remote upload failed with code $httpCode: " . ($response ?: 'No response body'));
|
||||
}
|
||||
|
||||
// Update storage records
|
||||
try
|
||||
{
|
||||
if ($fileName !== null)
|
||||
{
|
||||
IndexStorageManager::updateUploadName($uuid, $fileName);
|
||||
}
|
||||
|
||||
IndexStorageManager::updateUploadSize($uuid, $fileSize);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw new UploadException("Storage update failed", $e->getCode(), $e);
|
||||
}
|
||||
|
||||
// Clean up temporary file
|
||||
if ($isMultipart && file_exists($tmpName))
|
||||
{
|
||||
@unlink($tmpName);
|
||||
}
|
||||
|
||||
return [
|
||||
'filename' => $fileName,
|
||||
'download_url' => $response,
|
||||
'response_uuid' => $responseUuid,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the remote headers of the request
|
||||
*
|
||||
* @param string|null $fileName The file name to upload
|
||||
* @param string|null $token Optional. The upload token for Bearer authentication
|
||||
* @return string[] The result headers
|
||||
*/
|
||||
private static function buildRemoteHeaders(?string $fileName, ?string $token): array
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type: application/octet-stream',
|
||||
];
|
||||
|
||||
if ($fileName !== null)
|
||||
{
|
||||
$headers[] = 'X-Filename: ' . rawurlencode($fileName);
|
||||
}
|
||||
|
||||
if ($token !== null)
|
||||
{
|
||||
$headers[] = 'Authorization: Bearer ' . $token;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a header case-insensitively
|
||||
*
|
||||
* @param string $headerName The name of the header to get the value from
|
||||
* @return string|null The value of the header, null if not set.
|
||||
*/
|
||||
private static function getHeaderValue(string $headerName): ?string
|
||||
{
|
||||
$headers = getallheaders();
|
||||
foreach ($headers as $key => $value)
|
||||
{
|
||||
if (strtolower($key) === strtolower($headerName))
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function handleDownload(IndexStorageRecord $record): void
|
||||
{
|
||||
$pointers = $record->getPointers();
|
||||
if (empty($pointers) || !isset($pointers['download_url']))
|
||||
{
|
||||
throw new ServerException("No valid remote URL found in record");
|
||||
}
|
||||
|
||||
$fileSize = $record->getSize();
|
||||
$uploadPassword = Configuration::getProxyStorageConfiguration()->getUploadPassword();
|
||||
|
||||
// Set up cURL to stream the download
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $pointers['download_url'],
|
||||
CURLOPT_RETURNTRANSFER => false,
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_WRITEFUNCTION => function($ch, $data)
|
||||
{
|
||||
echo $data;
|
||||
return strlen($data);
|
||||
},
|
||||
CURLOPT_HTTPHEADER => $uploadPassword !== null ? ['Authorization: Bearer ' . $uploadPassword] : [],
|
||||
CURLOPT_TIMEOUT => Configuration::getProxyStorageConfiguration()->getCurlTimeout()
|
||||
]);
|
||||
|
||||
// Set appropriate headers for the download
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $record->getName() . '"');
|
||||
if ($fileSize > 0)
|
||||
{
|
||||
header('Content-Length: ' . $fileSize);
|
||||
}
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
|
||||
// Execute the request and stream directly to output
|
||||
$success = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if (!$success || ($httpCode < 200 || $httpCode >= 300))
|
||||
{
|
||||
throw new ServerException("Remote download failed: " . ($error ?: "HTTP $httpCode"), (int)$httpCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function handleDelete(IndexStorageRecord $record): void
|
||||
{
|
||||
$pointers = $record->getPointers();
|
||||
if (empty($pointers) || !isset($pointers['response_uuid']))
|
||||
{
|
||||
throw new ServerException("No valid UUID found in record for deletion");
|
||||
}
|
||||
|
||||
$uuid = $pointers['response_uuid'];
|
||||
$remoteUrl = Configuration::getProxyStorageConfiguration()->getEndpoint();
|
||||
|
||||
if (!$remoteUrl)
|
||||
{
|
||||
throw new ServerException("Remote endpoint not configured");
|
||||
}
|
||||
|
||||
// Add UUID as GET parameter
|
||||
$deleteUrl = rtrim($remoteUrl, '/') . '?' . http_build_query(['uuid' => $uuid]);
|
||||
$adminPassword = Configuration::getProxyStorageConfiguration()->getAdminPassword();
|
||||
|
||||
// Setup cURL options
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $deleteUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||||
CURLOPT_HTTPHEADER => $adminPassword !== null ? ['Authorization: Bearer ' . $adminPassword] : [],
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_TIMEOUT => Configuration::getProxyStorageConfiguration()->getCurlTimeout()
|
||||
]);
|
||||
|
||||
// Execute deletion request
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
// Handle errors
|
||||
if ($error)
|
||||
{
|
||||
throw new ServerException("cURL error during deletion: " . $error);
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300)
|
||||
{
|
||||
throw new ServerException("Remote deletion failed with code $httpCode: " . ($response ?: 'No response body'));
|
||||
}
|
||||
}
|
||||
}
|
431
src/FileServer/Classes/IndexStorageManager.php
Normal file
431
src/FileServer/Classes/IndexStorageManager.php
Normal file
|
@ -0,0 +1,431 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes;
|
||||
|
||||
use DateMalformedStringException;
|
||||
use DateTime;
|
||||
use FileServer\Enums\IndexStorageStatus;
|
||||
use FileServer\Enums\StorageType;
|
||||
use FileServer\Exceptions\DatabaseException;
|
||||
use FileServer\Objects\IndexStorageRecord;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
class IndexStorageManager
|
||||
{
|
||||
/**
|
||||
* Creates a new upload entry in the database.
|
||||
*
|
||||
* @param StorageType $type The storage type of the upload.
|
||||
* @param string|null $fileName Optional. The name of the file to be uploaded. If not provided, the UUID will be used as the filename.
|
||||
* @param int $size Optional. The size of the file to be uploaded in bytes. Default is 0.
|
||||
* @return string The UUID of the created upload entry.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
* @throws InvalidArgumentException If the provided filename is invalid or the size is negative.
|
||||
*/
|
||||
public static function createUpload(StorageType $type, ?string $fileName=null, int $size=0): string
|
||||
{
|
||||
if($fileName !== null)
|
||||
{
|
||||
$fileName = Validator::sanitizeFilename($fileName);
|
||||
if(!Validator::validFilename($fileName))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given filename "%s" is invalid.', $fileName));
|
||||
}
|
||||
}
|
||||
|
||||
if($size !== null && $size < 0)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given filename "%s" is too small.', $fileName));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$uploadStmt = Database::getConnection()->prepare('INSERT INTO uploads (uuid, storage_type, name, size) VALUES (:uuid, :storage_type, :name, :size)');
|
||||
$uploadStmt->bindValue(':uuid', $uuid = Uuid::v4()->toRfc4122());
|
||||
$uploadStmt->bindValue(':storage_type', $type->value);
|
||||
$uploadStmt->bindValue(':name', $fileName);
|
||||
$uploadStmt->bindValue(':size', $size);
|
||||
$uploadStmt->execute();
|
||||
|
||||
$statisticsStmt = Database::getConnection()->prepare('INSERT INTO statistics (uuid) VALUES (:uuid)');
|
||||
$statisticsStmt->bindValue(':uuid', $uuid);
|
||||
$statisticsStmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to create index.', 0, $e);
|
||||
}
|
||||
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an upload entry from the database by its UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry to retrieve.
|
||||
* @return IndexStorageRecord|null The upload entry, or null if not found.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function getUpload(string|IndexStorageRecord $uuid): ?IndexStorageRecord
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT * FROM uploads WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
$stmt->execute();
|
||||
|
||||
if($stmt->rowCount() === 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return IndexStorageRecord::fromArray($stmt->fetch(PDO::FETCH_ASSOC));
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to get index.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the download count for a given UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry.
|
||||
* @return int The download count.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function getDownloads(string|IndexStorageRecord $uuid): int
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT downloads FROM statistics WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
$stmt->execute();
|
||||
|
||||
return (int)$stmt->fetchColumn();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to get index.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last download date for a given UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry.
|
||||
* @return DateTime The last download date.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function getLastDownload(string|IndexStorageRecord $uuid): DateTime
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT last_download FROM statistics WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
$stmt->execute();
|
||||
|
||||
try
|
||||
{
|
||||
return new DateTime($stmt->fetchColumn());
|
||||
}
|
||||
catch (DateMalformedStringException $e)
|
||||
{
|
||||
throw new DatabaseException(sprintf('Failed to parse last_download column for %s', $uuid), $e);
|
||||
}
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to get index.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an upload entry exists in the database by its UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry to check.
|
||||
* @return bool True if the upload entry exists, false otherwise.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function uploadExists(string|IndexStorageRecord $uuid): bool
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM uploads WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
$stmt->execute();
|
||||
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to check index existence.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an upload entry from the database by its UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry to delete.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function deleteUpload(string|IndexStorageRecord $uuid): void
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('DELETE FROM uploads WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to delete index.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of upload entries from the database.
|
||||
*
|
||||
* @param int $page The page number to retrieve. Default is 1.
|
||||
* @param int $limit The number of entries per page. Default is 100.
|
||||
* @return IndexStorageRecord[] An array of IndexStorageRecord objects representing the upload entries.
|
||||
* @throws InvalidArgumentException If the provided page or limit is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function getUploads(int $page=1, int $limit=100): array
|
||||
{
|
||||
if($page < 1)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given page "%s" is invalid.', $page));
|
||||
}
|
||||
|
||||
if($limit < 1)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given limit "%s" is invalid.', $limit));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT * FROM uploads ORDER BY created LIMIT :offset, :limit');
|
||||
$stmt->bindValue(':offset', ($page - 1) * $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
return array_map(fn(array $row) => IndexStorageRecord::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to get indexes.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public static function updateUploadName(string|IndexStorageRecord $uuid, string $name): void
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('UPDATE uploads SET name=:name WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':name', $name);
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to update index size.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the upload size for a given UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry.
|
||||
* @param int $bytes The new size in bytes.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid or the size is negative.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function updateUploadSize(string|IndexStorageRecord $uuid, int $bytes): void
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('UPDATE uploads SET size=:size WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':size', $bytes);
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to update index size.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the upload status for a given UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry.
|
||||
* @param IndexStorageStatus $status The new status.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function updateUploadStatus(string|IndexStorageRecord $uuid, IndexStorageStatus $status): void
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('UPDATE uploads SET status=:status WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':status', $status->value);
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to update index status.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the upload pointers for a given UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry.
|
||||
* @param array $pointers The new pointers.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function updateUploadPointers(string|IndexStorageRecord $uuid, array $pointers): void
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('UPDATE uploads SET pointers=:pointers WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':pointers', json_encode($pointers));
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to update index pointers.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the download count for a given UUID.
|
||||
*
|
||||
* @param string|IndexStorageRecord $uuid The UUID of the upload entry.
|
||||
* @throws InvalidArgumentException If the provided UUID is invalid.
|
||||
* @throws DatabaseException If the database operation fails.
|
||||
*/
|
||||
public static function incrementDownload(string|IndexStorageRecord $uuid): void
|
||||
{
|
||||
if($uuid instanceof IndexStorageRecord)
|
||||
{
|
||||
$uuid = $uuid->getUuid();
|
||||
}
|
||||
elseif(!Uuid::isValid($uuid))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The given UUID "%s" is invalid.', $uuid));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('UPDATE statistics SET downloads=downloads+1 AND last_download=CURRENT_TIMESTAMP WHERE uuid=:uuid');
|
||||
$stmt->bindValue(':uuid', $uuid);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseException('Failed to increment download count.', 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/FileServer/Classes/Resources/database.sqlite
Normal file
BIN
src/FileServer/Classes/Resources/database.sqlite
Normal file
Binary file not shown.
14
src/FileServer/Classes/Resources/statistics.sql
Normal file
14
src/FileServer/Classes/Resources/statistics.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
create table statistics
|
||||
(
|
||||
uuid varchar(36) not null comment 'The UUID reference of the index storage'
|
||||
primary key comment 'The Unique Universal Identifier reference index',
|
||||
downloads bigint default 0 not null comment 'The amount the downloads the index has had',
|
||||
last_download timestamp null comment 'The Timestamp for when the file was last downloaded, null means the file has never been downloaded.',
|
||||
constraint statistics_uuid_uindex
|
||||
unique (uuid) comment 'The Unique Universal Identifier reference index',
|
||||
constraint statistics_index_storage_uuid_fk
|
||||
foreign key (uuid) references uploads (uuid)
|
||||
on update cascade on delete cascade
|
||||
)
|
||||
comment 'Table for housing statistics';
|
||||
|
25
src/FileServer/Classes/Resources/uploads.sql
Normal file
25
src/FileServer/Classes/Resources/uploads.sql
Normal file
|
@ -0,0 +1,25 @@
|
|||
create table uploads
|
||||
(
|
||||
uuid varchar(36) default uuid() not null comment 'The Primary Universal Unique Identifier for the file upload'
|
||||
primary key comment 'The Primary Unique Index for the UUID column',
|
||||
status enum ('UPLOADING', 'AVAILABLE', 'DELETED', 'MISSING') default 'UPLOADING' not null comment 'The status of the file
|
||||
- UPLOADING: The file is currently uploading, depending on the record creation date this can be used as a way to determine if the file upload is incomplete
|
||||
- AVAILABLE: The file is available for download
|
||||
- DELETED: The file was deleted from the server either manually or by the automated cleanup task
|
||||
- MISSING: The file once existed but can no longer be found on the server, this file was not deleted on purpose.',
|
||||
name varchar(255) null comment 'The name of the file, including the extension, if null it will fallback to the uuid with no extension',
|
||||
size bigint not null comment 'The size of the file in bytes',
|
||||
storage_type varchar(32) not null comment 'The storage type used to store the file',
|
||||
pointers blob null comment 'Pointer data for retrieving the file when requested',
|
||||
created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created',
|
||||
constraint uploads_uuid_uindex
|
||||
unique (uuid) comment 'The Primary Unique Index for the UUID column'
|
||||
)
|
||||
comment 'Table for housing indexes for file uploads';
|
||||
|
||||
create index uploads_created_index
|
||||
on uploads (created);
|
||||
|
||||
create index uploads_status_index
|
||||
on uploads (status);
|
||||
|
16
src/FileServer/Classes/Validator.php
Normal file
16
src/FileServer/Classes/Validator.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Classes;
|
||||
|
||||
class Validator
|
||||
{
|
||||
public static function sanitizeFilename(string $filename): string
|
||||
{
|
||||
// TODO: Implement this in the most secured and compatible way possible to prevent everything but the filename, excluding paths, just the filename itself.
|
||||
}
|
||||
|
||||
public static function validFilename(string $filename): bool
|
||||
{
|
||||
// TODO: Implement this to check if the given filename is a valid filename and does not include anything like a sub-path or escape strings or invalid/illegal characters
|
||||
}
|
||||
}
|
9
src/FileServer/Enums/DatabaseDriverType.php
Normal file
9
src/FileServer/Enums/DatabaseDriverType.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Enums;
|
||||
|
||||
enum DatabaseDriverType : string
|
||||
{
|
||||
case SQL = 'SQL';
|
||||
case SQLITE = 'SQLITE';
|
||||
}
|
11
src/FileServer/Enums/IndexStorageStatus.php
Normal file
11
src/FileServer/Enums/IndexStorageStatus.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Enums;
|
||||
|
||||
enum IndexStorageStatus : string
|
||||
{
|
||||
case UPLOADING = 'UPLOADING';
|
||||
case AVAILABLE = 'AVAILABLE';
|
||||
case DELETED = 'DELETED';
|
||||
case MISSING = 'MISSING';
|
||||
}
|
29
src/FileServer/Enums/RequestAction.php
Normal file
29
src/FileServer/Enums/RequestAction.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Enums;
|
||||
|
||||
enum RequestAction
|
||||
{
|
||||
case UPLOAD;
|
||||
case DOWNLOAD;
|
||||
case LIST;
|
||||
case DELETE;
|
||||
|
||||
/**
|
||||
* Attempts to parse the input from a string
|
||||
*
|
||||
* @param string $input The string input
|
||||
* @return RequestAction|null The result, null if no matches were found
|
||||
*/
|
||||
public static function fromString(string $input): ?RequestAction
|
||||
{
|
||||
return match(strtolower($input))
|
||||
{
|
||||
'upload' => self::UPLOAD,
|
||||
'download' => self::DOWNLOAD,
|
||||
'list' => self::LIST,
|
||||
'delete' => self::DELETE,
|
||||
default => null
|
||||
};
|
||||
}
|
||||
}
|
10
src/FileServer/Enums/StorageType.php
Normal file
10
src/FileServer/Enums/StorageType.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Enums;
|
||||
|
||||
enum StorageType : string
|
||||
{
|
||||
case LOCAL = 'LOCAL';
|
||||
case PROXY = 'PROXY';
|
||||
case CUSTOM = 'CUSTOM';
|
||||
}
|
17
src/FileServer/Exceptions/DatabaseException.php
Normal file
17
src/FileServer/Exceptions/DatabaseException.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class DatabaseException extends Exception
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct(string $message = "", ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $previous->getCode(), $previous);
|
||||
}
|
||||
}
|
17
src/FileServer/Exceptions/ServerException.php
Normal file
17
src/FileServer/Exceptions/ServerException.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class ServerException extends Exception
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct(string $message="", int $code=0, ?Throwable $previous=null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
28
src/FileServer/Exceptions/UploadException.php
Normal file
28
src/FileServer/Exceptions/UploadException.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class UploadException
|
||||
*
|
||||
* This exception is thrown when there is an internal error during the file upload process.
|
||||
*
|
||||
* @package FileServer\Exceptions
|
||||
*/
|
||||
class UploadException extends Exception
|
||||
{
|
||||
/**
|
||||
* UploadException constructor.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
* @param int $code The error code.
|
||||
* @param Throwable|null $previous The previous throwable used for exception chaining.
|
||||
*/
|
||||
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,513 @@
|
|||
|
||||
namespace FileServer;
|
||||
|
||||
use Exception;
|
||||
use FileServer\Classes\Configuration;
|
||||
use FileServer\Classes\Handlers\LocalHandler;
|
||||
use FileServer\Classes\Handlers\ProxyHandler;
|
||||
use FileServer\Classes\IndexStorageManager;
|
||||
use FileServer\Enums\IndexStorageStatus;
|
||||
use FileServer\Enums\RequestAction;
|
||||
use FileServer\Enums\StorageType;
|
||||
use FileServer\Exceptions\DatabaseException;
|
||||
use FileServer\Exceptions\ServerException;
|
||||
use FileServer\Interfaces\UploadHandlerInterface;
|
||||
use InvalidArgumentException;
|
||||
use ncc\Classes\Runtime;
|
||||
use ncc\Exceptions\ImportException;
|
||||
|
||||
class FileServer
|
||||
{
|
||||
/**
|
||||
* Handles the HTTP request for the FileServer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function handleRequest(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
switch(self::getRequestAction())
|
||||
{
|
||||
case RequestAction::UPLOAD:
|
||||
self::handleUploadRequest();
|
||||
break;
|
||||
|
||||
case RequestAction::DOWNLOAD:
|
||||
self::handleDownloadRequest();
|
||||
break;
|
||||
|
||||
case RequestAction::DELETE:
|
||||
self::handleDeleteRequest();
|
||||
break;
|
||||
|
||||
case RequestAction::LIST:
|
||||
self::handleListRequest();
|
||||
break;
|
||||
|
||||
default:
|
||||
self::basicResponse(400, 'Bad Request: Unknown request action');
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch(ServerException|DatabaseException $e)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: ' . $e->getMessage());
|
||||
}
|
||||
catch(InvalidArgumentException $e)
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the upload request
|
||||
*
|
||||
* @return void
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
private static function handleUploadRequest(): void
|
||||
{
|
||||
if(Configuration::getServerConfiguration()->isReadOnly())
|
||||
{
|
||||
self::basicResponse(403, 'Forbidden: Server is in read-only mode');
|
||||
return;
|
||||
}
|
||||
|
||||
if(Configuration::getServerConfiguration()->getUploadPassword() !== null)
|
||||
{
|
||||
if(self::getRequestPassword() === null)
|
||||
{
|
||||
self::basicResponse(401, 'Unauthorized: Authentication required', [
|
||||
sprintf("WWW-Authenticate: Bearer realm=\"%s\"", Configuration::getServerConfiguration()->getName())
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(self::getRequestPassword() !== Configuration::getServerConfiguration()->getUploadPassword())
|
||||
{
|
||||
self::basicResponse(403, 'Forbidden: Invalid upload password');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$indexStorageUuid = IndexStorageManager::createUpload(Configuration::getServerConfiguration()->getStorageType());
|
||||
|
||||
try
|
||||
{
|
||||
$pointers = match (Configuration::getServerConfiguration()->getStorageType())
|
||||
{
|
||||
StorageType::LOCAL => LocalHandler::handleUpload($indexStorageUuid),
|
||||
StorageType::PROXY => ProxyHandler::handleUpload($indexStorageUuid),
|
||||
StorageType::CUSTOM => function () use ($indexStorageUuid)
|
||||
{
|
||||
/** @var UploadHandlerInterface $class */
|
||||
$class = self::getCustomHandler();
|
||||
if ($class !== null)
|
||||
{
|
||||
yield $class::handleUpload($indexStorageUuid);
|
||||
}
|
||||
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exceptions\ServerException $e)
|
||||
{
|
||||
IndexStorageManager::deleteUpload($indexStorageUuid);
|
||||
self::basicResponse(400, 'Bad Request: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
catch (Exceptions\UploadException $e)
|
||||
{
|
||||
IndexStorageManager::deleteUpload($indexStorageUuid);
|
||||
self::basicResponse(500, 'Upload Error: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if(!is_array($pointers))
|
||||
{
|
||||
IndexStorageManager::deleteUpload($indexStorageUuid);
|
||||
self::basicResponse(500, 'Internal Server Error: Invalid upload handler response');
|
||||
return;
|
||||
}
|
||||
|
||||
IndexStorageManager::updateUploadStatus($indexStorageUuid, IndexStorageStatus::AVAILABLE);
|
||||
IndexStorageManager::updateUploadPointers($indexStorageUuid, $pointers);
|
||||
|
||||
// Generate a local URL for the uploaded file <base>/download?uuid=<uuid>
|
||||
self::basicResponse(200, sprintf('%s://%s/download?uuid=%s', $_SERVER['REQUEST_SCHEME'], $_SERVER['HTTP_HOST'], $indexStorageUuid), [
|
||||
'Content-Type: text/plain',
|
||||
'X-UUID: ' . $indexStorageUuid
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the download request
|
||||
*
|
||||
* @return void
|
||||
* @throws ServerException Thrown if there was a server-side exception
|
||||
*/
|
||||
private static function handleDownloadRequest(): void
|
||||
{
|
||||
if(!isset($_GET['uuid']))
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: Missing parameter \'uuid\'');
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$indexStorageRecord = IndexStorageManager::getUpload($_GET['uuid']);
|
||||
if($indexStorageRecord === null)
|
||||
{
|
||||
self::basicResponse(404, 'File Not Found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(InvalidArgumentException $e)
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: ' . get_class($e) . ' raised');
|
||||
return;
|
||||
}
|
||||
|
||||
switch($indexStorageRecord->getStorageType())
|
||||
{
|
||||
case StorageType::LOCAL:
|
||||
LocalHandler::handleDownload($indexStorageRecord);
|
||||
break;
|
||||
|
||||
case StorageType::PROXY:
|
||||
ProxyHandler::handleDownload($indexStorageRecord);
|
||||
break;
|
||||
|
||||
case StorageType::CUSTOM:
|
||||
/** @var UploadHandlerInterface $class */
|
||||
$class = self::getCustomHandler();
|
||||
if ($class === null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$class::handleDownload($indexStorageRecord);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a list request, password authentication will be prompted if one is set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function handleListRequest(): void
|
||||
{
|
||||
if(Configuration::getServerConfiguration()->getPassword() !== null)
|
||||
{
|
||||
if(self::getRequestPassword() === null)
|
||||
{
|
||||
self::basicResponse(401, 'Unauthorized: Authentication required', [
|
||||
sprintf("WWW-Authenticate: Bearer realm=\"%s\"", Configuration::getServerConfiguration()->getName())
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(self::getRequestPassword() !== Configuration::getServerConfiguration()->getPassword())
|
||||
{
|
||||
self::basicResponse(403, 'Forbidden: Invalid management password');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$page = 1;
|
||||
$limit = 100;
|
||||
|
||||
if(isset($_GET['page']))
|
||||
{
|
||||
$page = (int)$_GET['page'];
|
||||
if($page < 1)
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: Parameter \'page\' cannot be less than 1');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET['limit']))
|
||||
{
|
||||
$limit = (int)$_GET['limit'];
|
||||
if($limit < 1)
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: Parameter \'limit\' cannot be less than 1');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$results = IndexStorageManager::getUploads($page, $limit);
|
||||
}
|
||||
catch(DatabaseException)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: Failed to list uploads');
|
||||
return;
|
||||
}
|
||||
catch(InvalidArgumentException $e)
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$returnResults = [];
|
||||
foreach($results as $result)
|
||||
{
|
||||
try
|
||||
{
|
||||
$returnResults[] = [
|
||||
'uuid' => $result->getUuid(),
|
||||
'name' => $result->getName(),
|
||||
'size' => $result->getSize(),
|
||||
'created' => $result->getCreated()->getTimestamp(),
|
||||
'last_download' => IndexStorageManager::getLastDownload($result->getUuid()),
|
||||
'downloads' => IndexStorageManager::getDownloads($result->getUuid())
|
||||
];
|
||||
}
|
||||
catch (DatabaseException)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: Failed to retrieve statistics for ' . $result->getUuid());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self::basicResponse(
|
||||
code: 200,
|
||||
text: json_encode($returnResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_SLASHES),
|
||||
contentType: 'application/json'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a delete resource request, a password will be prompted if one is set
|
||||
*
|
||||
* @return void
|
||||
* @throws ServerException Thrown if there was a server-side exception
|
||||
*/
|
||||
private static function handleDeleteRequest(): void
|
||||
{
|
||||
if(Configuration::getServerConfiguration()->getPassword() !== null)
|
||||
{
|
||||
if(self::getRequestPassword() === null)
|
||||
{
|
||||
self::basicResponse(401, 'Unauthorized: Authentication required', [
|
||||
sprintf("WWW-Authenticate: Bearer realm=\"%s\"", Configuration::getServerConfiguration()->getName())
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(self::getRequestPassword() !== Configuration::getServerConfiguration()->getPassword())
|
||||
{
|
||||
self::basicResponse(403, 'Forbidden: Invalid upload password');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($_GET['uuid']))
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: Missing parameter \'uuid\'');
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$indexStorageRecord = IndexStorageManager::getUpload($_GET['uuid']);
|
||||
if($indexStorageRecord === null)
|
||||
{
|
||||
self::basicResponse(404, 'File Not Found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(InvalidArgumentException $e)
|
||||
{
|
||||
self::basicResponse(400, 'Bad Request: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: ' . get_class($e) . ' raised');
|
||||
return;
|
||||
}
|
||||
|
||||
switch($indexStorageRecord->getStorageType())
|
||||
{
|
||||
case StorageType::LOCAL:
|
||||
LocalHandler::handleDelete($indexStorageRecord);
|
||||
break;
|
||||
|
||||
case StorageType::PROXY:
|
||||
ProxyHandler::handleDelete($indexStorageRecord);
|
||||
break;
|
||||
|
||||
case StorageType::CUSTOM:
|
||||
/** @var UploadHandlerInterface $class */
|
||||
$class = self::getCustomHandler();
|
||||
if ($class === null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$class::handleDelete($indexStorageRecord);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom storage handler class name
|
||||
*
|
||||
* @return string|null The class name or null if not set
|
||||
*/
|
||||
private static function getCustomHandler(): ?string
|
||||
{
|
||||
if(Configuration::getCustomStorageConfiguration()->getPackage() !== null)
|
||||
{
|
||||
// Check if the package is set and exists
|
||||
if(!Runtime::isImported(Configuration::getCustomStorageConfiguration()->getPackage()))
|
||||
{
|
||||
try
|
||||
{
|
||||
Runtime::import(Configuration::getCustomStorageConfiguration()->getPackage());
|
||||
}
|
||||
catch(ImportException $e)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: Custom storage package not found, ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$class = Configuration::getCustomStorageConfiguration()->getClass();
|
||||
|
||||
// Check if the class is set and exists
|
||||
if($class === null)
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: Custom storage class not set');
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!class_exists($class))
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: Custom storage class not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the class implements UploadHandlerInterface
|
||||
if(!is_subclass_of($class, UploadHandlerInterface::class))
|
||||
{
|
||||
self::basicResponse(500, 'Internal Server Error: Custom storage class does not implement UploadHandlerInterface');
|
||||
return null;
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request password from the POST/GET parameters or from the HTTP headers
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private static function getRequestPassword(): ?string
|
||||
{
|
||||
// Check for password in GET or POST parameters
|
||||
if(isset($_GET['password']))
|
||||
{
|
||||
return $_GET['password'];
|
||||
}
|
||||
|
||||
elseif(isset($_POST['password']))
|
||||
{
|
||||
return $_POST['password'];
|
||||
}
|
||||
|
||||
// Check for password in HTTP headers
|
||||
if(isset($_SERVER['HTTP_AUTHORIZATION']))
|
||||
{
|
||||
return str_replace('Bearer ', '', $_SERVER['HTTP_AUTHORIZATION']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request action of the client
|
||||
*
|
||||
* @return RequestAction|null The request action that was detected null if the action couldn't be determined
|
||||
*/
|
||||
private static function getRequestAction(): ?RequestAction
|
||||
{
|
||||
return match (strtoupper($_SERVER['REQUEST_METHOD']))
|
||||
{
|
||||
'GET' => self::getUriAction($_GET) ?? RequestAction::DOWNLOAD,
|
||||
'POST' => self::getUriAction($_POST) ?? RequestAction::UPLOAD,
|
||||
'PUT' => RequestAction::UPLOAD,
|
||||
'DELETE' => RequestAction::DELETE,
|
||||
default => null,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract action from request parameters or URL path
|
||||
*
|
||||
* @param array $params Request parameters ($_GET or $_POST)
|
||||
* @return RequestAction|null The extracted action or null if not found
|
||||
*/
|
||||
private static function getUriAction(array $params): ?RequestAction
|
||||
{
|
||||
// Check for parameters
|
||||
if(isset($params['action']) || isset($params['a']))
|
||||
{
|
||||
return RequestAction::fromString($params['action'] ?? $params['a']);
|
||||
}
|
||||
|
||||
// Check for a trailing path in URL
|
||||
$path = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
|
||||
if(!empty($path))
|
||||
{
|
||||
$pathParts = explode('/', $path);
|
||||
$lastPart = end($pathParts);
|
||||
if(!empty($lastPart))
|
||||
{
|
||||
return RequestAction::fromString($lastPart);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a basic HTTP response with modifiable headers
|
||||
*
|
||||
* @param int $code The HTTP response code to return
|
||||
* @param string $text The HTTP body response to return
|
||||
* @param array|null $headers Optional. The headers to provide
|
||||
* @param string $contentType Optional. The content type, by default: text/plain
|
||||
* @return void
|
||||
*/
|
||||
public static function basicResponse(int $code, string $text, ?array $headers=null, string $contentType='text/plain'): void
|
||||
{
|
||||
http_response_code($code);
|
||||
if($headers !== null)
|
||||
{
|
||||
foreach($headers as $header)
|
||||
{
|
||||
header($header);
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('X-ServerName: ' . Configuration::getServerConfiguration()->getName());
|
||||
print($text);
|
||||
}
|
||||
}
|
21
src/FileServer/Interfaces/SerializableInterface.php
Normal file
21
src/FileServer/Interfaces/SerializableInterface.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Interfaces;
|
||||
|
||||
interface SerializableInterface
|
||||
{
|
||||
/**
|
||||
* Converts the object to an array representation.
|
||||
*
|
||||
* @return array The array representation of the object.
|
||||
*/
|
||||
public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Creates an object from an array representation.
|
||||
*
|
||||
* @param array $data The array representation of the object.
|
||||
* @return static The created object.
|
||||
*/
|
||||
public static function fromArray(array $data): self;
|
||||
}
|
38
src/FileServer/Interfaces/UploadHandlerInterface.php
Normal file
38
src/FileServer/Interfaces/UploadHandlerInterface.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Interfaces;
|
||||
|
||||
use FileServer\Exceptions\ServerException;
|
||||
use FileServer\Exceptions\UploadException;
|
||||
use FileServer\Objects\IndexStorageRecord;
|
||||
|
||||
interface UploadHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Handles the upload process.
|
||||
*
|
||||
* @param string $uuid The Unique Universal Identifier of the upload
|
||||
* @throws UploadException Thrown if there was a client error during the upload
|
||||
* @throws ServerException Thrown if there was a server error during the upload
|
||||
* @return array Pointers for the uploaded file, used for retrieving the file later
|
||||
*/
|
||||
public static function handleUpload(string $uuid): array;
|
||||
|
||||
/**
|
||||
* Handles the download process by transmitting the file over HTTP
|
||||
*
|
||||
* @param IndexStorageRecord $record The index storage record to download
|
||||
* @throws ServerException Thrown if there was a server error during the download
|
||||
* @return void
|
||||
*/
|
||||
public static function handleDownload(IndexStorageRecord $record): void;
|
||||
|
||||
/**
|
||||
* Handles the process of deleting the uploaded files from the server
|
||||
*
|
||||
* @param IndexStorageRecord $record The index storage record to delete
|
||||
* @throws ServerException Thrown if there was a server error during the delete operation
|
||||
* @return void
|
||||
*/
|
||||
public static function handleDelete(IndexStorageRecord $record): void;
|
||||
}
|
210
src/FileServer/Objects/IndexStorageRecord.php
Normal file
210
src/FileServer/Objects/IndexStorageRecord.php
Normal file
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Objects;
|
||||
|
||||
use DateMalformedStringException;
|
||||
use DateTime;
|
||||
use FileServer\Enums\IndexStorageStatus;
|
||||
use FileServer\Enums\StorageType;
|
||||
use FileServer\Interfaces\SerializableInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class IndexStorageRecord implements SerializableInterface
|
||||
{
|
||||
private string $uuid;
|
||||
private IndexStorageStatus $status;
|
||||
private ?string $name;
|
||||
private int $size;
|
||||
private StorageType $storageType;
|
||||
private ?array $pointers;
|
||||
private DateTime $created;
|
||||
|
||||
/**
|
||||
* IndexStorageRecord constructor.
|
||||
*
|
||||
* @param array $data The data to initialize the object with
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->uuid = $data['uuid'];
|
||||
$this->status = IndexStorageStatus::from(mb_strtoupper($data['status']));
|
||||
$this->name = $data['name'];
|
||||
$this->size = $data['size'];
|
||||
$this->storageType = StorageType::from(mb_strtoupper($data['storage_type']));
|
||||
$this->pointers = $data['pointers'];
|
||||
|
||||
try
|
||||
{
|
||||
$this->created = new DateTime($data['created']);
|
||||
}
|
||||
catch (DateMalformedStringException $e)
|
||||
{
|
||||
throw new InvalidArgumentException('The given created field is not a valid date.', $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UUID of the index storage record.
|
||||
*
|
||||
* @return string The UUID of the index storage record.
|
||||
*/
|
||||
public function getUuid(): string
|
||||
{
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UUID of the index storage record.
|
||||
*
|
||||
* @param string $uuid The UUID of the index storage record.
|
||||
*/
|
||||
public function setUuid(string $uuid): void
|
||||
{
|
||||
$this->uuid = $uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status of the index storage record.
|
||||
*
|
||||
* @return IndexStorageStatus The status of the index storage record.
|
||||
*/
|
||||
public function getStatus(): IndexStorageStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(IndexStorageStatus $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the index storage record.
|
||||
*
|
||||
* @return string The name of the index storage record.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
if($this->name === null)
|
||||
{
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the index storage record.
|
||||
*
|
||||
* @param string|null $name The name of the index storage record.
|
||||
*/
|
||||
public function setName(?string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the index storage record.
|
||||
*
|
||||
* @return int The size of the index storage record.
|
||||
*/
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the index storage record.
|
||||
*
|
||||
* @param int $size The size of the index storage record.
|
||||
*/
|
||||
public function setSize(int $size): void
|
||||
{
|
||||
$this->size = $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type of the index storage record.
|
||||
*
|
||||
* @return StorageType The storage type of the index storage record.
|
||||
*/
|
||||
public function getStorageType(): StorageType
|
||||
{
|
||||
return $this->storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the storage type of the index storage record.
|
||||
*
|
||||
* @param StorageType $storageType The storage type of the index storage record.
|
||||
*/
|
||||
public function setStorageType(StorageType $storageType): void
|
||||
{
|
||||
$this->storageType = $storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pointers of the index storage record.
|
||||
*
|
||||
* @return array|null The pointers of the index storage record.
|
||||
*/
|
||||
public function getPointers(): ?array
|
||||
{
|
||||
return $this->pointers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pointers of the index storage record.
|
||||
*
|
||||
* @param array|null $pointers The pointers of the index storage record.
|
||||
*/
|
||||
public function setPointers(?array $pointers): void
|
||||
{
|
||||
$this->pointers = $pointers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the created date of the index storage record.
|
||||
*
|
||||
* @return DateTime The created date of the index storage record.
|
||||
*/
|
||||
public function getCreated(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the created date of the index storage record.
|
||||
*
|
||||
* @param DateTime $created The created date of the index storage record.
|
||||
*/
|
||||
public function setCreated(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'status' => $this->status->value,
|
||||
'name' => $this->name,
|
||||
'size' => $this->size,
|
||||
'storage_type' => $this->storageType->value,
|
||||
'pointers' => $this->pointers,
|
||||
'created' => $this->created->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function fromArray(array $data): IndexStorageRecord
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
}
|
96
src/FileServer/Objects/UploadResult.php
Normal file
96
src/FileServer/Objects/UploadResult.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace FileServer\Objects;
|
||||
|
||||
use FileServer\Enums\StorageType;
|
||||
use FileServer\Interfaces\SerializableInterface;
|
||||
|
||||
class UploadResult implements SerializableInterface
|
||||
{
|
||||
private string $fileName;
|
||||
private int $fileSize;
|
||||
private StorageType $storageType;
|
||||
private array $pointers;
|
||||
|
||||
/**
|
||||
* Constructor for the UploadResult class.
|
||||
*
|
||||
* @param string $fileName The name of the uploaded file.
|
||||
* @param int $fileSize The size of the uploaded file in bytes.
|
||||
* @param StorageType $storageType The storage type of the uploaded file.
|
||||
* @param array $pointers The pointers to the uploaded file.
|
||||
*/
|
||||
public function __construct(string $fileName, int $fileSize, StorageType $storageType, array $pointers)
|
||||
{
|
||||
$this->fileName = $fileName;
|
||||
$this->fileSize = $fileSize;
|
||||
$this->storageType = $storageType;
|
||||
$this->pointers = $pointers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the uploaded file.
|
||||
*
|
||||
* @return string The name of the uploaded file.
|
||||
*/
|
||||
public function getFileName(): string
|
||||
{
|
||||
return $this->fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the uploaded file.
|
||||
*
|
||||
* @return int The size of the uploaded file in bytes.
|
||||
*/
|
||||
public function getFileSize(): int
|
||||
{
|
||||
return $this->fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage type of the uploaded file.
|
||||
*
|
||||
* @return StorageType The storage type of the uploaded file.
|
||||
*/
|
||||
public function getStorageType(): StorageType
|
||||
{
|
||||
return $this->storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pointers to the uploaded file.
|
||||
*
|
||||
* @return array The pointers to the uploaded file.
|
||||
*/
|
||||
public function getPointers(): array
|
||||
{
|
||||
return $this->pointers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'file_name' => $this->fileName,
|
||||
'file_size' => $this->fileSize,
|
||||
'storage_type' => $this->storageType->value,
|
||||
'pointers' => $this->pointers
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function fromArray(array $data): SerializableInterface
|
||||
{
|
||||
return new self(
|
||||
$data['file_name'],
|
||||
$data['file_size'],
|
||||
StorageType::from(mb_strtoupper($data['storage_type'])),
|
||||
$data['pointers']
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace FileServer;
|
||||
|
||||
use FileServer\Classes\Configuration;
|
||||
|
||||
class Program
|
||||
{
|
||||
/**
|
||||
|
@ -12,7 +14,7 @@
|
|||
*/
|
||||
public static function main(array $args): int
|
||||
{
|
||||
print("Hello World from net.nosial.file_server!" . PHP_EOL);
|
||||
Configuration::getConfigurationLib();
|
||||
return 0;
|
||||
}
|
||||
}
|
20
www/index.php
Normal file
20
www/index.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
# This is a simple single index file for loading in FileServer and passing on the request to be handled by the
|
||||
# server, modify this to your adjustments but this file in its current form will work
|
||||
|
||||
# Load ncc & import FileServer
|
||||
require 'ncc';
|
||||
import('net.nosial.fileserver');
|
||||
|
||||
try
|
||||
{
|
||||
# Pass on the request handler
|
||||
\FileServer\FileServer::handleRequest();
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
# Handle the exception
|
||||
http_response_code(500);
|
||||
print(sprintf("Internal Server Error: %s", $e->getMessage()));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue