Rewrite with Eloquent ORM JSON

This commit is contained in:
Axel 2023-05-22 17:28:26 +02:00
parent 52059e110d
commit 51a8831b98
Signed by: axel
GPG key ID: 73C0A5961B6BC740
35 changed files with 744 additions and 460 deletions

View file

@ -4,7 +4,6 @@ APP_KEY=
APP_DEBUG=false
APP_URL=
APP_TIMEZONE=Europe/Paris
APP_LOCALE=en
UPLOAD_MAX_FILESIZE=1G
UPLOAD_MAX_FILES=1000

View file

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Models\User;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
@ -42,8 +43,8 @@ class CreateUser extends Command
goto login;
}
// Checking login unicity
if (Storage::disk('users')->exists($login.'.json')) {
$existing = User::find($login);
if (! empty($existing) && $existing->count() > 0) {
$this->error('User "'.$login.'" already exists');
unset($login);
goto login;
@ -53,18 +54,17 @@ class CreateUser extends Command
// Asking for user's password
$password = $this->secret('Enter the user\'s password');
if (! preg_match('~^.{4,100}$i~', $password)) {
$this->error('Invalid password format. Must contains between 5 and 100 chars');
if (! preg_match('~^[^\s]{5,100}$~', $password)) {
$this->error('Invalid password format. Must contains between 5 and 100 chars without space');
unset($password);
goto password;
}
try {
Storage::disk('users')->put($login.'.json', json_encode([
User::create([
'username' => $login,
'password' => Hash::make($password),
'bundles' => []
]));
'password' => Hash::make($password)
]);
$this->info('User has been created');
}

View file

@ -5,8 +5,9 @@ namespace App\Helpers;
use Exception;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use App\Models\User;
class User {
class Auth {
static function isLogged():Bool {
// Checking credentials auth
@ -27,7 +28,7 @@ class User {
$user = self::getUserDetails($username);
// Checking password
if (true !== Hash::check($password, $user['password'])) {
if (true !== Hash::check($password, $user->password)) {
throw new Exception('Invalid password');
}
@ -41,30 +42,19 @@ class User {
}
}
static function getLoggedUserDetails():Array {
static function getLoggedUserDetails():User {
if (self::isLogged()) {
return self::getUserDetails(session()->get('username'));
}
throw new UnauthenticatedUser('User is not logged in');
}
static function getUserDetails(String $username):Array {
// Checking user existence
if (Storage::disk('users')->missing($username.'.json')) {
static function getUserDetails(String $username):User {
$user = User::find($username);
if (empty($user)) {
throw new Exception('No such user');
}
// Getting user.json
if (! $json = Storage::disk('users')->get($username.'.json')) {
throw new Exception('Could not fetch user details');
}
// Decoding JSON
if (! $user = json_decode($json, true)) {
throw new Exception('Cannot decode JSON file');
}
return $user;
}

View file

@ -70,7 +70,7 @@ class Upload {
return $metadata;
}
public static function humanFilesize(Float $size, Int $precision = 2):Int {
public static function humanFilesize(Float $size, Int $precision = 2):String {
if ($size > 0) {
$size = (int) $size;
$base = log($size) / log(1024);

View file

@ -4,31 +4,20 @@ namespace App\Http\Controllers;
use ZipArchive;
use Exception;
use Carbon\Carbon;
use App\Helpers\Upload;
use App\Models\Bundle;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Resources\BundleResource;
class BundleController extends Controller
{
// The bundle content preview
public function previewBundle(Request $request, $bundleId) {
// Getting bundle metadata
abort_if(! $metadata = Upload::getMetadata($bundleId), 404);
// Handling dates as Carbon
Carbon::setLocale(config('app.locale'));
$metadata['created_at_carbon'] = Carbon::createFromTimestamp($metadata['created_at']);
$metadata['expires_at_carbon'] = Carbon::createFromTimestamp($metadata['expires_at']);
public function previewBundle(Request $request, Bundle $bundle) {
return view('download', [
'bundleId' => $bundleId,
'metadata' => $metadata,
'auth' => $metadata['preview_token']
'bundle' => new BundleResource($bundle)
]);
}
@ -36,23 +25,18 @@ class BundleController extends Controller
// The download method
// - the bundle
// - or just one file
public function downloadZip(Request $request, $bundleId) {
// Getting bundle metadata
abort_if(! $metadata = Upload::getMetadata($bundleId), 404);
public function downloadZip(Request $request, Bundle $bundle) {
try {
// Download of the full bundle
// We must create a Zip archive
Upload::setMetadata($bundleId, [
'downloads' => $metadata['downloads'] + 1
]);
$bundle->downloads ++;
$bundle->save();
$filename = config('filesystems.disks.uploads.root').'/'.$metadata['bundle_id'].'/bundle.zip';
if (1 == 1 || ! file_exists($filename)) {
// Timestamped filename
$filename = Storage::disk('uploads')->path('').'/'.$bundle->slug.'/bundle.zip';
if (! file_exists($filename)) {
$bundlezip = fopen($filename, 'w');
//chmod($filename, 0600);
// Creating the archive
$zip = new ZipArchive;
@ -61,14 +45,14 @@ class BundleController extends Controller
}
// Setting password when required
if (! empty($metadata['password'])) {
$zip->setPassword($metadata['password']);
if (! empty($bundle->password)) {
$zip->setPassword($bundle->password);
}
// Adding the files into the Zip with their real names
foreach ($metadata['files'] as $k => $file) {
if (file_exists(config('filesystems.disks.uploads.root').'/'.$file['fullpath'])) {
$name = $file['original'];
foreach ($bundle->files as $k => $file) {
if (file_exists(config('filesystems.disks.uploads.root').'/'.$file->fullpath)) {
$name = $file->original;
// If a file in the archive has the same name
if (false !== $zip->locateName($name)) {
@ -89,9 +73,9 @@ class BundleController extends Controller
$name = $newname;
}
// Finally adding files
$zip->addFile(config('filesystems.disks.uploads.root').'/'.$file['fullpath'], $name);
$zip->addFile(config('filesystems.disks.uploads.root').'/'.$file->fullpath, $name);
if (! empty($metadata['password'])) {
if (! empty($bundle->password)) {
$zip->setEncryptionIndex($k, ZipArchive::EM_AES_256);
}
}
@ -116,16 +100,17 @@ class BundleController extends Controller
$limit_rate = $filesize;
}
// Flushing everything
flush();
// Let's download now
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.Str::slug($metadata['title']).'-'.time().'.zip'.'"');
header('Content-Disposition: attachment; filename="'.Str::slug($bundle->title).'-'.time().'.zip'.'"');
header('Cache-Control: no-cache, must-revalidate');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
header('Content-Length: '.$filesize);
flush();
// Downloading
$fh = fopen($filename, 'rb');
while (! feof($fh)) {
echo fread($fh, round($limit_rate));

View file

@ -8,44 +8,44 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Resources\BundleResource;
use App\Http\Resources\FileResource;
use App\Models\Bundle;
use App\Models\File;
class UploadController extends Controller
{
public function createBundle(Request $request, String $bundleId = null) {
$metadata = Upload::getMetadata($bundleId);
abort_if(empty($metadata), 404);
public function createBundle(Request $request, Bundle $bundle) {
return view('upload', [
'metadata' => $metadata ?? null,
'bundle' => $bundle->toArray(),
'baseUrl' => config('app.url')
]);
}
public function getMetadata(Request $request, String $bundleId) {
return response()->json(Upload::getMetadata($bundleId));
}
// The upload form
public function storeBundle(Request $request, String $bundleId) {
public function storeBundle(Request $request, Bundle $bundle) {
$metadata = [
'expiry' => $request->expiry ?? null,
'password' => $request->password ?? null,
'title' => $request->title ?? null,
'description' => $request->description ?? null,
'max_downloads' => $request->max_downloads ?? 0
];
try {
$bundle->update([
'expiry' => $request->expiry ?? null,
'password' => $request->password ?? null,
'title' => $request->title ?? null,
'description' => $request->description ?? null,
'max_downloads' => $request->max_downloads ?? 0
]);
$metadata = Upload::setMetaData($bundleId, $metadata);
// Creating the bundle folder
Storage::disk('uploads')->makeDirectory($bundleId);
return response()->json($metadata);
return response()->json(new BundleResource($bundle));
}
catch (Exception $e) {
return response()->json([
'result' => false,
'message' => $e->getMessage()
], 500);
}
}
public function uploadFile(Request $request, String $bundleId) {
public function uploadFile(Request $request, Bundle $bundle) {
// Validating form data
$request->validate([
@ -59,22 +59,23 @@ class UploadController extends Controller
// Moving file to final destination
try {
$fullpath = $request->file('file')->storeAs(
$bundleId, $filename, 'uploads'
);
$size = Storage::disk('uploads')->size($fullpath);
$size = $request->file->getSize();
if (config('sharing.upload_prevent_duplicates', true) === true && $size < Upload::humanReadableToBytes(config('sharing.hash_maxfilesize', '1G'))) {
$hash = sha1_file(Storage::disk('uploads')->path($fullpath));
if (Upload::isDuplicateFile($bundleId, $hash)) {
Storage::disk('uploads')->delete($fullpath);
$hash = sha1_file($request->file->getPathname());
$existing = $bundle->files->whereNotNull('hash')->where('hash', $hash)->count();
if (! empty($existing) && $existing > 0) {
throw new Exception(__('app.duplicate-file'));
}
}
$fullpath = $request->file('file')->storeAs(
$bundle->slug, $filename, 'uploads'
);
// Generating file metadata
$file = [
$file = new File([
'uuid' => $request->uuid,
'bundle_slug' => $bundle->slug,
'original' => $original,
'filesize' => $size,
'fullpath' => $fullpath,
@ -82,78 +83,72 @@ class UploadController extends Controller
'created_at' => time(),
'status' => true,
'hash' => $hash ?? null
];
$metadata = Upload::addFileMetaData($bundleId, $file);
return response()->json([
'result' => true,
'file' => $file
]);
$file->save();
return response()->json(new FileResource($file));
}
catch (Exception $e) {
return response()->json([
'result' => false,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
'message' => $e->getMessage()
], 500);
}
}
public function deleteFile(Request $request, String $bundleId) {
public function deleteFile(Request $request, Bundle $bundle) {
$request->validate([
'uuid' => 'required|uuid'
]);
try {
$metadata = Upload::deleteFile($bundleId, $request->uuid);
return response()->json($metadata);
// Getting file model
$file = File::findOrFail($request->uuid);
// Physically deleting the file
if (! Storage::disk('uploads')->delete($file->fullpath)) {
throw new Exception('Cannot delete file from disk');
}
// Destroying the model
$file->delete();
return response()->json(new BundleResource($bundle));
}
catch (Exception $e) {
return response()->json([
'result' => false,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
'message' => $e->getMessage()
], 500);
}
}
public function completeBundle(Request $request, String $bundleId) {
$metadata = Upload::getMetadata($bundleId);
public function completeBundle(Request $request, Bundle $bundle) {
// Processing size
if (! empty($metadata['files'])) {
$size = 0;
foreach ($metadata['files'] as $f) {
$size += $f['filesize'];
}
$size = 0;
foreach ($bundle->files as $f) {
$size += $f['filesize'];
}
// Saving metadata
try {
$preview_token = substr(sha1(uniqid('dbdl', true)), 0, rand(10, 15));
$bundle->completed = true;
$bundle->expires_at = time()+$bundle->expiry;
$bundle->fullsize = $size;
$bundle->preview_link = route('bundle.preview', ['bundle' => $bundle, 'auth' => $bundle->preview_token]);
$bundle->download_link = route('bundle.zip.download', ['bundle' => $bundle, 'auth' => $bundle->preview_token]);
$bundle->deletion_link = route('upload.bundle.delete', ['bundle' => $bundle]);
$bundle->save();
$metadata = Upload::setMetadata($bundleId, [
'completed' => true,
'expires_at' => time()+$metadata['expiry'],
'fullsize' => $size,
'preview_token' => $preview_token,
'preview_link' => route('bundle.preview', ['bundle' => $bundleId, 'auth' => $preview_token]),
'download_link' => route('bundle.zip.download', ['bundle' => $bundleId, 'auth' => $preview_token]),
'deletion_link' => route('upload.bundle.delete', ['bundle' => $bundleId])
]);
return response()->json($metadata);
return response()->json(new BundleResource($bundle));
}
catch (\Exception $e) {
return response()->json([
'result' => false,
'error' => $e->getMessage()
'message' => $e->getMessage()
], 500);
}
}
@ -164,23 +159,25 @@ class UploadController extends Controller
* We invalidate the expiry date and let the CRON
* task do the hard work
*/
public function deleteBundle(Request $request, $bundleId) {
public function deleteBundle(Request $request, Bundle $bundle) {
// Tries to get the metadata file
$metadata = Upload::getMetadata($bundleId);
// Forcing file to expire
$metadata['expires_at'] = time() - (3600 * 24 * 30);
// Rewriting the metadata file
try {
$metadata = Upload::setMetadata($bundleId, $metadata);
return response()->json($metadata);
// Forcing bundle to expire
$bundle->expires_at = time() - (3600 * 24 * 30);
$bundle->save();
// Then deleting file models
foreach ($bundle->files as $f) {
$f->forceDelete();
}
return response()->json(new BundleResource($bundle));
}
catch (Exception $e) {
return response()->json([
'success' => false
]);
'success' => false,
'message' => $e->getMessage()
], 500);
}
}

View file

@ -1,16 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\Upload;
use App\Helpers\User;
use App\Helpers\Auth;
use Exception;
use Illuminate\Http\Request;
use App\Http\Resources\BundleResource;
use App\Models\Bundle;
class WebController extends Controller
{
public function homepage()
{
return view('homepage');
// Getting user bundles
if (Auth::isLogged()) {
$bundles = Auth::getLoggedUserDetails()->bundles;
if (! empty($bundles) && $bundles->count() > 0) {
$bundles = BundleResource::collection($bundles) ;
}
}
return view('homepage', [
'bundles' => $bundles ?? []
]);
}
public function login() {
@ -26,7 +36,7 @@ class WebController extends Controller
]);
try {
if (true === User::loginUser($request->login, $request->password)) {
if (true === Auth::loginUser($request->login, $request->password)) {
return response()->json([
'result' => true,
]);
@ -35,63 +45,55 @@ class WebController extends Controller
catch (Exception $e) {
return response()->json([
'result' => false,
'error' => 'Authentication failed, please try again.'
'message' => 'Authentication failed, please try again.'
], 403);
}
// This should never happen
return response()->json([
'result' => false,
'error' => 'Unexpected error'
]);
'message' => 'Unexpected error'
], 500);
}
function newBundle(Request $request) {
// Aborting if request is not AJAX
abort_if(! $request->ajax(), 403);
$request->validate([
'bundle_id' => 'required',
'owner_token' => 'required'
]);
$owner = null;
if (User::isLogged()) {
$user = User::getLoggedUserDetails();
$owner = $user['username'];
// If bundle dimension is not initialized
if (empty($user['bundles']) || ! is_array($user['bundles'])) {
$user['bundles'] = [];
}
array_push($user['bundles'], $request->bundle_id);
User::setUserDetails($user['username'], $user);
if (Auth::isLogged()) {
$user = Auth::getLoggedUserDetails();
}
$metadata = [
'owner' => $owner,
'created_at' => time(),
'completed' => false,
'expiry' => config('sharing.default-expiry', 86400),
'expires_at' => null,
'password' => null,
'bundle_id' => $request->bundle_id,
'owner_token' => $request->owner_token,
'preview_token' => null,
'fullsize' => 0,
'files' => [],
'title' => null,
'description' => null,
'max_downloads' => 0,
'downloads' => 0
];
try {
$bundle = new Bundle([
'user_username' => $user->username ?? null,
'created_at' => time(),
'completed' => false,
'expiry' => config('sharing.default-expiry', 86400),
'expires_at' => null,
'password' => null,
'slug' => substr(sha1(uniqid('slug_', true)), 0, rand(35, 40)),
'owner_token' => substr(sha1(uniqid('preview_', true)), 0, 15),
'preview_token' => substr(sha1(uniqid('preview_', true)), 0, 15),
'fullsize' => 0,
'title' => null,
'description' => null,
'max_downloads' => 0,
'downloads' => 0
]);
$bundle->save();
if (Upload::setMetadata($metadata['bundle_id'], $metadata)) {
return response()->json($metadata);
return response()->json([
'result' => true,
'redirect' => route('upload.create.show', ['bundle' => $bundle->slug]),
'bundle' => new BundleResource($bundle)
]);
}
else {
abort(500);
catch (Exception $e) {
return response()->json([
'result' => false,
'message' => $e->getMessage()
], 500);
}
}
}

View file

@ -21,6 +21,7 @@ class Kernel extends HttpKernel
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\Localisation::class
];
/**

View file

@ -6,6 +6,8 @@ use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Helpers\Upload;
use App\Models\Bundle;
use Carbon\Carbon;
class GuestAccess
{
@ -17,27 +19,23 @@ class GuestAccess
public function handle(Request $request, Closure $next): Response
{
// Aborting if Bundle ID is not present
abort_if(empty($request->route()->parameter('bundle')), 403);
abort_if(empty($request->route()->parameter('bundle')), 404);
$bundle = $request->route()->parameters()['bundle'];
abort_if(! is_a($bundle, Bundle::class), 404);
// Aborting if Auth token is not provided
abort_if(empty($request->auth), 403);
// Getting metadata
$metadata = Upload::getMetadata($request->route()->parameter('bundle'));
// Aborting if metadata are empty
abort_if(empty($metadata), 404);
// Aborting if auth_token is different from URL param
abort_if($metadata['preview_token'] !== $request->auth, 403);
abort_if($bundle->preview_token !== $request->auth, 403);
// Checking bundle expiration
abort_if($metadata['expires_at'] < time(), 404);
// Aborting if bundle expired
abort_if($bundle->expires_at->isBefore(Carbon::now()), 404);
// If there is no file into the bundle (should never happen but ...)
abort_if(count($metadata['files']) == 0, 404);
abort_if(($metadata['max_downloads'] ?? 0) > 0 && $metadata['downloads'] >= $metadata['max_downloads'], 404);
// Aborting if max download is reached
abort_if( ($bundle->max_downloads ?? 0) > 0 && $bundle->downloads >= $bundle->max_downloads, 404);
// Else resuming
return $next($request);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\App;
class Localisation
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$locales = $request->header('accept-language');
if (! empty($locales)) {
if (preg_match_all('~([a-z]{2})[-|_]?~', $locales, $matches) && ! empty($matches[1])) {
$locales = array_unique($matches[1]);
foreach ($locales as $l) {
if (in_array($l, config('app.supported_locales'))) {
App::setLocale($l);
break;
}
}
}
}
return $next($request);
}
}

View file

@ -6,6 +6,7 @@ use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Helpers\Upload;
use App\Models\Bundle;
class OwnerAccess
{
@ -21,6 +22,8 @@ class OwnerAccess
// Aborting if Bundle ID is not present
abort_if(empty($request->route()->parameter('bundle')), 403);
$bundle = $request->route()->parameters()['bundle'];
abort_if(! is_a($bundle, Bundle::class), 404);
// Aborting if auth is not present
$auth = null;
@ -30,16 +33,11 @@ class OwnerAccess
else if (! empty($request->auth)) {
$auth = $request->auth;
}
// Aborting if no auth token provided
abort_if(empty($auth), 403);
// Getting metadata
$metadata = Upload::getMetadata($request->route()->parameter('bundle'));
// Aborting if metadata are empty
abort_if(empty($metadata), 404);
// Aborting if auth_token is different from URL param
abort_if($metadata['owner_token'] !== $auth, 403);
// Aborting if owner token is wrong
abort_if($bundle->owner_token !== $auth, 403);
return $next($request);
}

View file

@ -6,7 +6,7 @@ use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Helpers\Upload;
use App\Helpers\User;
use App\Helpers\Auth;
use Illuminate\Support\Facades\Storage;
class UploadAccess
@ -26,7 +26,7 @@ class UploadAccess
}
// Checking credentials auth
if (User::isLogged()) {
if (Auth::isLogged()) {
return $next($request);
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Route;
use App\Http\Resources\UserResource;
use App\Http\Resources\FileResource;
class BundleResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
/**
Do not return private data on the preview page
*/
$full = false;
$middleware = Route::current()->gatherMiddleware('access.guest');
foreach ($middleware as $m) {
if ($m === 'access.owner') {
$full = true;
}
}
$response = [
'created_at' => $this->created_at,
'completed' => (bool)$this->completed,
'expiry' => (int)$this->expiry,
'expires_at' => $this->expires_at,
'slug' => $this->slug,
'fullsize' => (int)$this->fullsize,
'title' => $this->title,
'description' => $this->description,
'max_downloads' => (int)$this->max_downloads,
'downloads' => (int)$this->downloads,
'files' => FileResource::collection($this->files),
'preview_link' => $this->preview_link,
'download_link' => $this->download_link,
'password' => $this->when($full === true, $this->password),
'owner_token' => $this->when($full === true, $this->owner_token),
'preview_token' => $this->when($full === true, $this->preview_token),
'deletion_link' => $this->when($full === true, $this->deletion_link),
'user' => $this->when($full === true, new UserResource($this->user))
];
return $response;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FileResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'uuid' => $this->uuid,
'bundle_slug' => $this->bundle_slug,
'original' => $this->original,
'filesize' => (int)$this->filesize,
'fullpath' => $this->fullpath,
'filename' => $this->filename,
'created_at' => $this->created_at,
'status' => $this->status,
'hash' => $this->hash
];
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'username' => $this->username
];
}
}

73
app/Models/Bundle.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use \Orbit\Concerns\Orbital;
use Illuminate\Database\Schema\Blueprint;
class Bundle extends Model
{
use Orbital;
public $incrementing = false;
public $fillable = [
'user_username',
'created_at',
'completed',
'expiry',
'expires_at',
'password' ,
'slug',
'owner_token',
'preview_token',
'fullsize',
'title',
'description',
'max_downloads',
'downloads',
'preview_link',
'download_link',
'deletion_link'
];
protected $casts = [
'expires_at' => 'datetime:Y-m-d',
];
public function getKeyName() {
return 'slug';
}
public function getIncrementing() {
return false;
}
public static function schema(Blueprint $table) {
$table->string('slug');
$table->string('title')->nullable();
$table->longText('description')->nullable();
$table->string('password')->nullable();
$table->string('owner_token');
$table->string('preview_token');
$table->integer('fullsize')->default(0);
$table->integer('max_downloads')->nullable();
$table->integer('downloads')->default(0);
$table->boolean('completed')->default(false);
$table->integer('expiry')->default(0);
$table->timestamp('expires_at')->nullable();
$table->string('preview_link')->nullable();
$table->string('download_link')->nullable();
$table->string('deletion_link')->nullable();
$table->string('user_username')->nullable();
}
public function files() {
return $this->hasMany(File::class);
}
public function user() {
return $this->belongsTo(User::class);
}
}

53
app/Models/File.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use \Orbit\Concerns\Orbital;
use Illuminate\Database\Schema\Blueprint;
class File extends Model
{
use Orbital;
public $fillable = [
'uuid',
'bundle_slug',
'original',
'filesize',
'fullpath',
'filename',
'created_at',
'status',
'hash'
];
public $incrementing = false;
public function getKeyName()
{
return 'uuid';
}
public function getIncrementing()
{
return false;
}
public static function schema(Blueprint $table)
{
$table->string('uuid');
$table->string('original')->nullable();
$table->string('filename')->nullable();
$table->string('status')->nullable();
$table->string('hash')->nullable();
$table->longText('fullpath')->nullable();
$table->boolean('filesize')->nullable();
$table->string('bundle_slug');
}
public function bundle() {
return $this->belongsTo(Bundle::class);
}
}

View file

@ -2,15 +2,16 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use \Orbit\Concerns\Orbital;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
use Orbital;
/**
* The attributes that are mass assignable.
@ -18,8 +19,7 @@ class User extends Authenticatable
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'username',
'password',
];
@ -30,15 +30,30 @@ class User extends Authenticatable
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public $incrementing = false;
public function getKeyName()
{
return 'username';
}
public function getIncrementing()
{
return false;
}
public static function schema(Blueprint $table)
{
$table->string('username');
$table->string('password');
}
public function bundles() {
return $this->hasMany(Bundle::class);
}
}

View file

@ -19,6 +19,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
}
}

View file

@ -24,6 +24,7 @@ class RouteServiceProvider extends ServiceProvider
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

View file

@ -98,6 +98,18 @@ return [
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application supported locales
|--------------------------------------------------------------------------
|
| List of all the supported locales of this application
|
*/
'supported_locales' => [
'en', 'fr'
],
/*
|--------------------------------------------------------------------------
| Faker Locale

18
config/orbit.php Normal file
View file

@ -0,0 +1,18 @@
<?php
return [
'default' => env('ORBIT_DEFAULT_DRIVER', 'json'),
'drivers' => [
'md' => \Orbit\Drivers\Markdown::class,
'json' => \Orbit\Drivers\Json::class,
'yaml' => \Orbit\Drivers\Yaml::class,
],
'paths' => [
'content' => base_path('content'),
'cache' => storage_path('framework/cache/orbit'),
],
];

View file

@ -61,6 +61,7 @@ return [
'created-at' => 'Created',
'fullsize' => 'Total',
'max-downloads' => 'Max downloads',
'current-downloads' => 'Downloads',
'create-new-upload' => 'Create a new upload bundle',
'page-not-found' => 'Page not found',
'permission-denied' => 'Permission denied',
@ -81,5 +82,8 @@ return [
'password' => 'Password',
'do-login' => 'Login now',
'pending' => 'Drafts',
'duplicate-file' => 'This file already exists in the bundle'
'duplicate-file' => 'This file already exists in the bundle',
'unexpected-error' => 'An unexpected error has occurred',
'login-to-get-bundles' => 'to get your bundles',
'you-are-logged-in' => 'You are logged in as ":username"'
];

View file

@ -61,6 +61,7 @@ return [
'created-at' => 'Créé',
'fullsize' => 'Total',
'max-downloads' => 'Téléchargements maximum',
'current-downloads' => 'Téléchargements',
'create-new-upload' => 'Créer une nouvelle archive',
'page-not-found' => 'Page non trouvée',
'permission-denied' => 'Permission refusée',
@ -79,7 +80,10 @@ return [
'authentication' => 'Authentification',
'login' => 'Identifiant',
'password' => 'Mot de passe',
'do-login' => 'S\'authentifier',
'do-login' => 'Authentifiez-vous',
'pending' => 'Brouillons',
'duplicate-file' => 'Ce fichier existe déjà dans l\'archive'
'duplicate-file' => 'Ce fichier existe déjà dans l\'archive',
'unexpected-error' => 'Une erreur inattendue est survenue',
'to-get-bundles' => 'pour accéder à vos archives',
'you-are-logged-in' => 'Vous êtes connecté(e) en tant que ":username"'
];

View file

@ -6,6 +6,8 @@ import axios from 'axios';
window.axios = axios;
import moment from 'moment';
import 'moment/locale/fr';
moment.locale('fr');
window.moment = moment;
moment().format();

View file

@ -4,14 +4,13 @@
@push('scripts')
<script>
let auth = @js($auth);
let bundleId = @js($bundleId);
let bundle = @js($bundle);
let bundle_expires = '{{ __('app.warning-bundle-expiration') }}'
let bundle_expired = '{{ __('app.warning-bundle-expired') }}'
document.addEventListener('alpine:init', () => {
Alpine.data('download', () => ({
metadata: @js($metadata),
metadata: @js($bundle),
created_at: null,
expires_at: null,
expired: null,
@ -25,13 +24,10 @@
},
updateTimes: function() {
this.created_at = moment.unix(this.metadata.created_at).fromNow()
this.created_at = moment(this.metadata.created_at).fromNow()
if (this.isExpired()) {
this.expires_at = bundle_expired
}
else {
this.expires_at = bundle_expires+' '+moment.unix(this.metadata.expires_at).fromNow()
if (! this.isExpired()) {
this.expires_at =moment(this.metadata.expires_at).fromNow()
}
},
@ -78,26 +74,50 @@
@lang('app.preview-bundle')
</h2>
<div class="flex flex-wrap items-center">
<p class="w-6/12 px-1">
<div class="flex flex-wrap justify-between items-center text-xs">
<p class="w-full px-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.upload-title')
</span>
<span x-text="metadata.title"></span>
</p>
<p class="w-4/12 px-1">
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.created-at')
</span>
<span x-text="created_at"></span>
</p>
<p class="w-2/12 px-1">
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.upload-expiry')
</span>
<span x-text="expires_at"></span>
</p>
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.fullsize')
</span>
<span x-text="humanSize(metadata.fullsize)"></span>
</p>
<p class="w-full px-1" x-show="metadata.description">
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.max-downloads')
</span>
<span x-text="metadata.max_downloads > 0 ? metadata.max_download : '∞'"></span>
</p>
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.current-downloads')
</span>
<span x-text="metadata.downloads"></span>
</p>
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.password')
</span>
<span x-text="metadata.password ? 'yes': 'no'"></span>
</p>
<p class="w-full px-1 mt-1" x-show="metadata.description">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.upload-description')
</span>
@ -121,12 +141,7 @@
<div class="grid grid-cols-2 gap-10 mt-10 text-center items-center">
<div>
<p class="font-xs font-medium">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="inline w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span x-text="expires_at"></span>
</p>
&nbsp;
</div>
<div>
@include('partials.button', [

View file

@ -1,19 +1,10 @@
@extends('layout')
@section('content')
<div>
<div class="relative bg-white border border-primary rounded-lg overflow-hidden">
<div class="bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase flex items-center">
<div class="grow text-center">{{ config('app.name') }}</div>
</h1>
</div>
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">403</h1>
@lang('app.permission-denied')
</div>
</div>
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">403</h1>
@lang('app.permission-denied')
</div>
@endsection

View file

@ -1,19 +1,10 @@
@extends('layout')
@section('content')
<div>
<div class="relative bg-white border border-primary rounded-lg overflow-hidden">
<div class="bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase flex items-center">
<div class="grow text-center">{{ config('app.name') }}</div>
</h1>
</div>
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">404</h1>
@lang('app.page-not-found')
</div>
</div>
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">404</h1>
@lang('app.page-not-found')
</div>
@endsection

View file

@ -0,0 +1,10 @@
@extends('layout')
@section('content')
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">500</h1>
@lang('app.unexpected-error')
</div>
@endsection

View file

@ -1,4 +1,11 @@
<footer class="relative mt-5 h-6">
@if (App\Helpers\Auth::isLogged())
<span class="ml-3 text-xs text-slate-600">
@lang('app.you-are-logged-in', [
'username' => App\Helpers\Auth::getLoggedUserDetails()['username']
])
</span>
@endif
<div class="absolute right-0 top-0 text-[.6rem] text-slate-100 text-right px-2 py-1 italic bg-primary rounded-tl-lg">
Made with

View file

@ -1,7 +1,10 @@
<header class="bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<a href="/">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase flex items-center">
<div class="grow text-center">{{ config('app.name') }}</div>
</h1>
</a>
<header class="relative bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase">
<div class="grow text-center">
<a href="/">
{{ config('app.name') }}
</a>
</div>
</h1>
</header>

View file

@ -3,22 +3,29 @@
@push('scripts')
<script>
let bundles = @js($bundles)
document.addEventListener('alpine:init', () => {
Alpine.data('bundle', () => ({
bundles: null,
bundles: [],
pending: [],
active: [],
expired: [],
currentBundle: null,
ownerToken: null,
init: function() {
// Getting bundles stored locally
bundles = localStorage.getItem('bundles');
// And JSON decoding it
this.bundles = JSON.parse(bundles)
// Generating anonymous owner token
this.ownerToken = localStorage.getItem('owner_token')
if (this.ownerToken === null) {
this.ownerToken = this.generateStr(15)
localStorage.setItem('owner_token', this.ownerToken)
}
// Loading existing bundles
this.bundles = bundles
if (this.bundles != null && Object.keys(this.bundles).length > 0) {
this.bundles.forEach( (bundle) => {
if (bundle.title == null || bundle.title == '') {
bundle.label = 'untitled'
@ -27,7 +34,7 @@
bundle.label = bundle.title
}
if (bundle.expires_at != null && moment.unix(bundle.expires_at).isBefore(moment())) {
if (bundle.expires_at != null && moment(bundle.expires_at).isBefore(moment())) {
this.expired.push(bundle)
}
else if (bundle.completed == true) {
@ -36,38 +43,20 @@
else {
this.pending.push(bundle)
}
bundle.label += ' - {{ __('app.created-at') }} '+moment.unix(bundle.created_at).fromNow()
bundle.label += ' - {{ __('app.created-at') }} '+moment(bundle.created_at).fromNow()
})
}
// If bundle is empty, initializing it
if (this.bundles == null || this.bundles == '') {
this.bundles = []
}
},
newBundle: function() {
// Generating a new bundle key pair
const pair = {
bundle_id: this.generateStr(30),
owner_token: this.generateStr(15),
created_at: moment().unix()
}
this.bundles.unshift(pair)
// Storing them locally
localStorage.setItem('bundles', JSON.stringify(this.bundles))
axios({
url: '/new',
method: 'POST',
data: {
bundle_id: pair.bundle_id,
owner_token: pair.owner_token
}
method: 'POST'
})
.then( (response) => {
window.location.href = '/upload/'+response.data.bundle_id
if (response.data.result === true) {
window.location.href = response.data.redirect
}
})
.catch( (error) => {
//TODO: do something here
@ -107,46 +96,59 @@
@section('content')
<div x-data="bundle">
<div class="p-5">
<h2 class="font-title text-2xl mb-5 text-primary font-medium uppercase flex items-center">
<p>@lang('app.existing-bundles')</p>
<p class="text-sm bg-primary rounded-full ml-2 text-white px-3" x-text="Object.keys(bundles).length"></p>
</h2>
<span x-show="bundles == null || Object.keys(bundles).length == 0">@lang('app.no-existing-bundle')</span>
<select
class="w-full py-4 text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50"
name="expiry"
id="upload-expiry"
x-model="currentBundle"
x-on:change="redirectToBundle()"
x-show="bundles != null && Object.keys(bundles).length > 0"
>
<option>-</option>
<div>
<h2 class="font-title text-2xl mb-5 text-primary font-medium uppercase flex items-center">
<p>@lang('app.existing-bundles')</p>
<p class="text-sm bg-primary rounded-full ml-2 text-white px-3" x-text="Object.keys(bundles).length"></p>
</h2>
<template x-if="Object.keys(pending).length > 0">
<optgroup label="{{ __('app.pending') }}">
<template x-for="bundle in pending">
<option :value="bundle.bundle_id" x-text="bundle.label"></option>
@if (App\Helpers\Auth::isLogged())
<p class="text-center">
<span x-show="bundles == null || Object.keys(bundles).length == 0">@lang('app.no-existing-bundle')</span>
</p>
<select
class="w-full py-4 text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50"
name="expiry"
id="upload-expiry"
x-model="currentBundle"
x-on:change="redirectToBundle()"
x-show="bundles != null && Object.keys(bundles).length > 0"
>
<option>-</option>
<template x-if="Object.keys(pending).length > 0">
<optgroup label="{{ __('app.pending') }}">
<template x-for="bundle in pending">
<option :value="bundle.slug" x-text="bundle.label"></option>
</template>
</optgroup>
</template>
</optgroup>
</template>
<template x-if="Object.keys(active).length > 0">
<optgroup label="{{ __('app.active') }}">
<template x-for="bundle in active">
<option :value="bundle.bundle_id" x-text="bundle.label"></option>
<template x-if="Object.keys(active).length > 0">
<optgroup label="{{ __('app.active') }}">
<template x-for="bundle in active">
<option :value="bundle.slug" x-text="bundle.label"></option>
</template>
</optgroup>
</template>
</optgroup>
</template>
<template x-if="Object.keys(expired).length > 0">
<optgroup label="{{ __('app.expired') }}">
<template x-for="bundle in expired">
<option :value="bundle.bundle_id" x-text="bundle.label"></option>
<template x-if="Object.keys(expired).length > 0">
<optgroup label="{{ __('app.expired') }}">
<template x-for="bundle in expired">
<option :value="bundle.slug" x-text="bundle.label"></option>
</template>
</optgroup>
</template>
</optgroup>
</template>
</select>
</select>
@else
<p class="text-center">
<a href="/login" class="text-primary font-bold hover:underline">@lang('app.do-login')</a>
@lang('app.to-get-bundles')
</p>
@endif
</div>
<h2 class="mt-10 font-title text-2xl mb-5 text-primary font-medium uppercase">@lang('app.or-create')</h2>

View file

@ -43,9 +43,9 @@
if (response.data.result == true) {
window.location.href = '/'
}
else {
this.error = response.data.error
}
})
.catch( (error) => {
this.error = error.response.data.message
})
}
}))

View file

@ -3,20 +3,18 @@
@section('page_title', __('app.upload-files-title'))
@push('scripts')
<script>
let baseUrl = @js($baseUrl);
let metadata = @js($metadata ?? []);
let bundle = @js($bundle);
let maxFiles = @js(config('sharing.max_files'));
let maxFileSize = @js(Upload::fileMaxSize());
document.addEventListener('alpine:init', () => {
Alpine.data('upload', () => ({
bundle: null,
bundleIndex: null,
bundles: null,
dropzone: null,
uploadedFiles: [],
metadata: [],
completed: false,
step: 0,
maxFiles: maxFiles,
@ -43,14 +41,14 @@
],
init: function() {
this.metadata = metadata
this.bundle = bundle
if (this.getBundle()) {
// Steps router
if (this.metadata.completed == true) {
if (this.bundle.completed == true) {
this.step = 3
}
else if (this.metadata.title) {
else if (this.bundle.title) {
this.step = 2
this.startDropzone()
}
@ -61,39 +59,6 @@
},
getBundle: function() {
// Getting all bundles store in local storage
bundles = localStorage.getItem('bundles')
// If no bundle found, back to homepage
if (bundles == null || bundles == '') {
window.location.href = '/'
return false
}
this.bundles = JSON.parse(bundles)
// Looking for the current bundle
if (this.bundles != null && Object.keys(this.bundles).length > 0) {
this.bundles.forEach( (element, index) => {
if (element.bundle_id == this.metadata.bundle_id) {
//this.bundle = Object.assign(element)
//this.bundleIndex = index
this.bundle = index
}
})
}
// If current bundle not found, aborting
if (this.bundle == null) {
window.location.href = '/'
return false
}
if (this.bundles[this.bundle].owner_token != this.metadata.owner_token) {
window.location.href = '/'
return false
}
return true
},
@ -105,17 +70,17 @@
document.getElementById('upload-password').setCustomValidity('')
document.getElementById('upload-max-downloads').setCustomValidity('')
if (this.metadata.title == null || this.metadata.title == '') {
if (this.bundle.title == null || this.bundle.title == '') {
document.getElementById('upload-title').setCustomValidity('Field is required')
errors = true
}
if (this.metadata.expiry == null || this.metadata.expiry == '') {
if (this.bundle.expiry == null || this.bundle.expiry == '') {
document.getElementById('upload-expiry').setCustomValidity('Field is required')
errors = true
}
if (this.metadata.max_downloads < 0 || this.metadata.max_downloads > 999) {
if (this.bundle.max_downloads < 0 || this.bundle.max_downloads > 999) {
document.getElementById('upload-max-downloads').setCustomValidity('Invalid number of max downloads')
errors = true
}
@ -125,20 +90,20 @@
}
axios({
url: '/upload/'+this.metadata.bundle_id,
url: '/upload/'+this.bundle.slug,
method: 'POST',
data: {
expiry: this.metadata.expiry,
title: this.metadata.title,
description: this.metadata.description,
max_downloads: this.metadata.max_downloads,
password: this.metadata.password,
auth: this.bundles[this.bundle].owner_token
expiry: this.bundle.expiry,
title: this.bundle.title,
description: this.bundle.description,
max_downloads: this.bundle.max_downloads,
password: this.bundle.password,
auth: this.bundle.owner_token
}
})
.then( (response) => {
this.syncData(response.data)
window.history.pushState(null, null, baseUrl+'/upload/'+this.metadata.bundle_id);
window.history.pushState(null, null, baseUrl+'/upload/'+this.bundle.slug);
this.step = 2
this.startDropzone()
@ -149,16 +114,16 @@
},
completeStep: function() {
if (Object.keys(this.metadata.files).length == 0) {
if (Object.keys(this.bundle.files).length == 0) {
return false;
}
this.showModal('{{ __('app.confirm-complete') }}', () => {
axios({
url: '/upload/'+this.metadata.bundle_id+'/complete',
url: '/upload/'+this.bundle.slug+'/complete',
method: 'POST',
data: {
auth: this.bundles[this.bundle].owner_token
auth: this.bundle.owner_token
}
})
@ -183,10 +148,10 @@
this.maxFiles = this.maxFiles - this.countFilesOnServer() >= 0 ? this.maxFiles - this.countFilesOnServer() : 0
this.dropzone = new Dropzone('#upload-frm', {
url: '/upload/'+this.metadata.bundle_id+'/file',
url: '/upload/'+this.bundle.slug+'/file',
method: 'POST',
headers: {
'X-Upload-Auth': this.bundles[this.bundle].owner_token
'X-Upload-Auth': this.bundle.owner_token
},
createImageThumbnails: false,
disablePreviews: true,
@ -204,7 +169,7 @@
this.dropzone.on('addedfile', (file) => {
file.uuid = this.uuid()
this.metadata.files.unshift({
this.bundle.files.unshift({
uuid: file.uuid,
original: file.name,
filesize: file.size,
@ -224,29 +189,29 @@
let fileIndex = null
if (fileIndex = this.findFileIndex(file.uuid)) {
this.metadata.files[fileIndex].progress = Math.round(progress)
this.bundle.files[fileIndex].progress = Math.round(progress)
}
})
this.dropzone.on('error', (file, message) => {
let fileIndex = this.findFileIndex(file.uuid)
this.metadata.files[fileIndex].status = false
this.bundle.files[fileIndex].status = false
if (message.hasOwnProperty('error')) {
this.metadata.files[fileIndex].message = message.error
if (message.hasOwnProperty('message')) {
this.bundle.files[fileIndex].message = message.message
}
else {
this.metadata.files[fileIndex].message = message
this.bundle.files[fileIndex].message = message
}
})
this.dropzone.on('complete', (file) => {
let fileIndex = this.findFileIndex(file.uuid)
this.metadata.files[fileIndex].progress = 0
this.bundle.files[fileIndex].progress = 0
if (file.status == 'success') {
this.maxFiles--
this.metadata.files[fileIndex].status = true
this.bundle.files[fileIndex].status = true
}
})
}
@ -259,11 +224,11 @@
let lfile = file
axios({
url: '/upload/'+this.metadata.bundle_id+'/file',
url: '/upload/'+this.bundle.slug+'/file',
method: 'DELETE',
data: {
uuid: lfile.uuid,
auth: this.bundles[this.bundle].owner_token
auth: this.bundle.owner_token
}
})
.then( (response) => {
@ -277,7 +242,7 @@
// File not valid, no need to remove it from server, just locally
else if (file.status == false) {
let fileIndex = this.findFileIndex(file.uuid)
this.metadata.files.splice(fileIndex, 1)
this.bundle.files.splice(fileIndex, 1)
}
// File has not being uploaded, cannot delete file yet
else {
@ -288,16 +253,17 @@
deleteBundle: function() {
this.showModal('{{ __('app.confirm-delete-bundle') }}', () => {
axios({
url: '/upload/'+this.metadata.bundle_id+'/delete',
url: '/upload/'+this.bundle.slug+'/delete',
method: 'DELETE',
data: {
auth: this.bundles[this.bundle].owner_token
auth: this.bundle.owner_token
}
})
.then( (response) => {
if (! response.data.success) {
this.syncData(response.data)
}
this.syncData(response.data)
})
.catch( (error) => {
})
})
},
@ -305,25 +271,23 @@
findFile: function(uuid) {
let index = this.findFileIndex(uuid)
if (index != null) {
return this.metadata.files[index]
return this.bundle.files[index]
}
return null
},
findFileIndex: function (uuid) {
for (i in this.metadata.files) {
if (this.metadata.files[i].uuid == uuid) {
for (i in this.bundle.files) {
if (this.bundle.files[i].uuid == uuid) {
return i
}
}
return null
},
syncData: function(metadata) {
if (Object.keys(metadata).length > 0) {
this.metadata = metadata
this.bundles[this.bundle] = metadata
localStorage.setItem('bundles', JSON.stringify(this.bundles))
syncData: function(bundle) {
if (Object.keys(bundle).length > 0) {
this.bundle = bundle
}
},
@ -376,9 +340,9 @@
countFilesOnServer: function() {
count = 0
if (this.metadata.hasOwnProperty('files') && Object.keys(this.metadata.files).length > 0) {
for (i in this.metadata.files) {
if (this.metadata.files[i].status == true) {
if (this.bundle.hasOwnProperty('files') && Object.keys(this.bundle.files).length > 0) {
for (i in this.bundle.files) {
if (this.bundle.files[i].status == true) {
count ++
}
}
@ -387,11 +351,11 @@
},
isBundleExpired: function() {
if (this.metadata.expires_at == null || this.metadata.expires_at == '') {
if (this.bundle.expires_at == null || this.bundle.expires_at == '') {
return false;
}
return moment.unix(this.metadata.expires_at).isBefore(moment())
return moment.unix(this.bundle.expires_at).isBefore(moment())
}
}))
})
@ -457,7 +421,7 @@
</p>
<input
x-model="metadata.title"
x-model="bundle.title"
class="w-full p-0 bg-transparent text-slate-700 h-8 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50"
type="text"
name="title"
@ -471,7 +435,7 @@
<span class="font-title uppercase">@lang('app.upload-description')</span>
<textarea
x-model="metadata.description"
x-model="bundle.description"
maxlength="300"
class="w-full p-0 bg-transparent text-slate-700 h-18 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50"
type="text"
@ -489,7 +453,7 @@
</p>
<select
x-model="metadata.expiry""
x-model="bundle.expiry""
class="w-full text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50"
name="expiry"
id="upload-expiry"
@ -508,7 +472,7 @@
</p>
<input
x-model="metadata.max_downloads"
x-model="bundle.max_downloads"
class="w-full p-0 bg-transparent text-slate-700 h-8 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50"
type="number"
name="max_downloads"
@ -523,7 +487,7 @@
<span class="font-title uppercase">@lang('app.bundle-password')</span>
<input
x-model="metadata.password"
x-model="bundle.password"
class="w-full bg-transparent text-slate-700 h-8 p-0 py-1 rounded-none border-b border-primary-superlight outline-none invalid:border-b-red-500 invalid:bg-red-50"
placeholder="@lang('app.leave-empty')"
type="text"
@ -581,11 +545,11 @@
</div>
</h3>
<span class="text-xs text-slate-400" x-show="Object.keys(metadata.files).length == 0">@lang('app.no-file')</span>
<span class="text-xs text-slate-400" x-show="Object.keys(bundle.files).length == 0">@lang('app.no-file')</span>
{{-- Files list --}}
<ul id="output" class="text-xs max-h-32 overflow-y-scroll pb-3" x-show="Object.keys(metadata.files).length > 0">
<template x-for="(f, k) in metadata.files" :key="k">
<ul id="output" class="text-xs max-h-32 overflow-y-scroll pb-3" x-show="Object.keys(bundle.files).length > 0">
<template x-for="(f, k) in bundle.files" :key="k">
<li
title="{{ __('app.click-to-remove') }}"
class="relative flex items-center leading-5 list-inside even:bg-gray-50 rounded px-2 cursor-pointer overflow-hidden"
@ -663,7 +627,7 @@
@lang('app.preview-link')
</div>
<div class="w-2/3 shadow">
<input x-model="metadata.preview_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" />
<input x-model="bundle.preview_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" />
</div>
</div>
@ -673,7 +637,7 @@
@lang('app.direct-link')
</div>
<div class="w-2/3 shadow">
<input x-model="metadata.download_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" />
<input x-model="bundle.download_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" />
</div>
</div>

View file

@ -7,6 +7,7 @@ use App\Http\Controllers\UploadController;
use App\Http\Controllers\BundleController;
use App\Http\Middleware\UploadAccess;
/*
|--------------------------------------------------------------------------
| Web Routes
@ -18,31 +19,40 @@ use App\Http\Middleware\UploadAccess;
|
*/
/**
Public route for login
*/
Route::get('/login', [WebController::class, 'login']);
Route::post('/login', [WebController::class, 'doLogin']);
/**
Upload routes
*/
Route::middleware(['can.upload'])->group(function() {
Route::get('/', [WebController::class, 'homepage'])->name('homepage');
Route::post('/new', [WebController::class, 'newBundle'])->name('bundle.new');
Route::get('/new', [WebController::class, 'newBundle'])->name('bundle.new');
Route::prefix('/upload/{bundle}')->controller(UploadController::class)->name('upload.')->group(function() {
Route::get('/', 'createBundle')->name('create.show');
Route::get('/', 'createBundle')->name('create.show');
Route::middleware(['access.owner'])->group(function() {
Route::post('/', 'storeBundle')->name('create.store');
Route::get('/metadata', 'getMetadata')->name('metadata.get');
Route::post('/file', 'uploadFile')->name('file.store');
Route::delete('/file', 'deleteFile')->name('file.delete');
Route::post('/complete', 'completeBundle')->name('complete');
Route::post('/', 'storeBundle')->name('create.store');
Route::get('/metadata', 'getMetadata')->name('metadata.get');
Route::post('/file', 'uploadFile')->name('file.store');
Route::delete('/file', 'deleteFile')->name('file.delete');
Route::post('/complete', 'completeBundle')->name('complete');
Route::delete('/delete', 'deleteBundle')->name('bundle.delete');
});
});
});
/**
Download routes
*/
Route::middleware(['access.guest'])->prefix('/bundle/{bundle}')->controller(BundleController::class)->name('bundle.')->group(function() {
Route::get('/preview', 'previewBundle')->name('preview');
Route::post('/zip', 'prepareZip')->name('zip.make');
Route::get('/download', 'downloadZip')->name('zip.download');
Route::get('/preview', 'previewBundle')->name('preview');
Route::post('/zip', 'prepareZip')->name('zip.make');
Route::get('/download', 'downloadZip')->name('zip.download');
});