feat: implement APK versioning and management with upload, update, and download functionalities
This commit is contained in:
parent
5ec3e6d52a
commit
19af2f4bef
@ -14,7 +14,7 @@ services:
|
||||
- DB_DATABASE=${DB_DATABASE}
|
||||
- DB_PORT=${DB_PORT}
|
||||
volumes:
|
||||
- storage_data:/var/www/repuve-backend-v1/storage
|
||||
- ../../storage:/var/www/repuve-backend-v1/storage
|
||||
networks:
|
||||
- repuve-network
|
||||
mem_limit: 512M
|
||||
@ -67,8 +67,6 @@ services:
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
storage_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
repuve-network:
|
||||
|
||||
@ -17,7 +17,9 @@ RUN apk add --no-cache \
|
||||
mysql-client \
|
||||
libreoffice \
|
||||
ttf-dejavu \
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
|
||||
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
|
||||
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ services:
|
||||
- DB_DATABASE=${DB_DATABASE}
|
||||
- DB_PORT=${DB_PORT}
|
||||
volumes:
|
||||
- storage_data:/var/www/repuve-backend-v1/storage
|
||||
- ../../storage:/var/www/repuve-backend-v1/storage
|
||||
networks:
|
||||
- repuve-network
|
||||
mem_limit: 512M
|
||||
@ -29,7 +29,7 @@ services:
|
||||
- "127.0.0.1:${NGINX_PORT}:80"
|
||||
volumes:
|
||||
- ../../public:/var/www/repuve-backend-v1/public
|
||||
- storage_data:/var/www/repuve-backend-v1/storage
|
||||
- ../../storage:/var/www/repuve-backend-v1/storage
|
||||
- ../../Docker/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- /var/log/nginx:/var/log/nginx
|
||||
logging:
|
||||
@ -65,8 +65,6 @@ services:
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
storage_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
repuve-network:
|
||||
|
||||
@ -14,7 +14,9 @@ RUN apk add --no-cache \
|
||||
bash \
|
||||
libreoffice \
|
||||
ttf-dejavu \
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
|
||||
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
|
||||
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
||||
@ -119,12 +119,11 @@ http {
|
||||
fastcgi_param HTTP_X_REQUEST_ID $request_id;
|
||||
}
|
||||
|
||||
client_max_body_size 20M;
|
||||
client_max_body_size 150M;
|
||||
|
||||
# Handle storage files (Laravel storage link)
|
||||
location /storage {
|
||||
alias /var/www/repuve-backend-v1/storage/app/public;
|
||||
try_files $uri =404;
|
||||
location /storage/ {
|
||||
alias /var/www/repuve-backend-v1/storage/app/public/;
|
||||
}
|
||||
|
||||
location /profile {
|
||||
|
||||
@ -4,40 +4,74 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\ApkStorageRequest;
|
||||
use App\Http\Requests\Repuve\ApkUpdateRequest;
|
||||
use App\Models\ApkLog;
|
||||
use App\Models\ApkVersion;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class AppController extends Controller
|
||||
class AppController extends Controller implements HasMiddleware
|
||||
{
|
||||
|
||||
/**
|
||||
* Middleware
|
||||
*/
|
||||
public static function middleware(): array
|
||||
{
|
||||
return [
|
||||
self::can('apk.upload', ['upload']),
|
||||
self::can('apk.index', ['index']),
|
||||
self::can('apk.edit', ['update']),
|
||||
self::can('apk.destroy', ['destroy']),
|
||||
self::can('apk.download', ['download']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subir APK de la aplicación móvil
|
||||
* Listar versiones: actual, 2 anteriores y las no disponibles
|
||||
*/
|
||||
public function upload(ApkStorageRequest $request)
|
||||
public function index()
|
||||
{
|
||||
$latestId = ApkVersion::latest()->value('id');
|
||||
|
||||
$versions = ApkVersion::with('uploader:id,name')
|
||||
->withCount('logs')
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($version) use ($latestId) {
|
||||
$version->is_current = $version->id === $latestId;
|
||||
$version->available = $version->id === $latestId;
|
||||
return $version;
|
||||
});
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'current' => $versions->where('is_current', true)->first(),
|
||||
'recent' => $versions->where('is_current', false)->take(2)->values(),
|
||||
'unavailable' => $versions->where('is_current', false)->skip(2)->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subir nueva versión del APK
|
||||
*/
|
||||
public function store(ApkStorageRequest $request)
|
||||
{
|
||||
$file = $request->file('apk');
|
||||
|
||||
// Eliminar APK anterior si existe
|
||||
$existingFiles = Storage::disk('public')->files('apk');
|
||||
foreach ($existingFiles as $existingFile) {
|
||||
Storage::disk('public')->delete($existingFile);
|
||||
$fileName = 'repuve-app-' . now()->format('Y-m-d') . '.apk';
|
||||
|
||||
// Eliminar APK anterior del storage
|
||||
foreach (Storage::disk('public')->files('apk') as $existing) {
|
||||
Storage::disk('public')->delete($existing);
|
||||
}
|
||||
|
||||
// Guardar el nuevo APK
|
||||
$fileName = 'repuve-app-' . now()->format('Y-m-d') . '.apk';
|
||||
$file->storeAs('apk', $fileName, 'public');
|
||||
$path = $file->storeAs('apk', $fileName, 'public');
|
||||
|
||||
ApkVersion::create([
|
||||
'file_name' => $fileName,
|
||||
'path' => $path,
|
||||
'changelog' => $request->input('changelog'),
|
||||
'uploaded_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'APK subido correctamente',
|
||||
@ -46,19 +80,64 @@ public function upload(ApkStorageRequest $request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Descargar APK de la aplicación móvil
|
||||
* Actualizar nombre y/o changelog de una versión
|
||||
*/
|
||||
public function download(): BinaryFileResponse
|
||||
public function update(ApkUpdateRequest $request, ApkVersion $app)
|
||||
{
|
||||
$files = Storage::disk('public')->files('apk');
|
||||
$data = $request->only(['changelog', 'file_name']);
|
||||
|
||||
if (empty($files)) {
|
||||
if ($request->filled('file_name') && $request->input('file_name') !== $app->file_name) {
|
||||
$newFileName = $request->input('file_name');
|
||||
$newPath = 'apk/' . $newFileName;
|
||||
|
||||
if (Storage::disk('public')->exists($newPath)) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Ya existe un archivo con ese nombre.',
|
||||
]);
|
||||
}
|
||||
|
||||
Storage::disk('public')->move($app->path, $newPath);
|
||||
|
||||
$data['file_name'] = $newFileName;
|
||||
$data['path'] = $newPath;
|
||||
}
|
||||
|
||||
$app->update($data);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Versión actualizada correctamente',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar una versión
|
||||
*/
|
||||
public function destroy(ApkVersion $app)
|
||||
{
|
||||
Storage::disk('public')->delete($app->path);
|
||||
$app->delete();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Versión eliminada correctamente',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Descargar APK más reciente y registrar log
|
||||
*/
|
||||
public function download()
|
||||
{
|
||||
$version = ApkVersion::latest()->first();
|
||||
|
||||
if (!$version) {
|
||||
abort(404, 'No hay APK disponible para descargar.');
|
||||
}
|
||||
|
||||
$latestFile = end($files);
|
||||
$path = Storage::disk('public')->path($latestFile);
|
||||
ApkLog::create([
|
||||
'apk_version_id' => $version->id,
|
||||
'downloaded_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->download($path);
|
||||
return redirect(asset('storage/' . $version->path));
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ public function authorize()
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'apk' => 'required|file|max:102400',
|
||||
'apk' => 'required|file|max:153600',
|
||||
];
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ public function messages()
|
||||
return [
|
||||
'apk.required' => 'El archivo APK es obligatorio',
|
||||
'apk.file' => 'El archivo debe ser un archivo válido',
|
||||
'apk.max' => 'El archivo no debe superar los 100MB',
|
||||
'apk.max' => 'El archivo no debe superar los 150MB',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
31
app/Http/Requests/Repuve/ApkUpdateRequest.php
Normal file
31
app/Http/Requests/Repuve/ApkUpdateRequest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ApkUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return auth()->user()->can('apk.edit');
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'file_name' => 'nullable|string|max:255',
|
||||
'changelog' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'file_name.string' => 'El nombre del archivo debe ser una cadena de texto',
|
||||
'file_name.max' => 'El nombre del archivo no debe superar los 255 caracteres',
|
||||
'changelog.string' => 'El changelog debe ser una cadena de texto',
|
||||
'changelog.max' => 'El changelog no debe superar los 255 caracteres',
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Models/ApkLog.php
Normal file
32
app/Models/ApkLog.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class ApkLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'apk_version_id',
|
||||
'downloaded_by',
|
||||
];
|
||||
|
||||
public function apkVersion()
|
||||
{
|
||||
return $this->belongsTo(ApkVersion::class, 'apk_version_id');
|
||||
}
|
||||
|
||||
public function downloader()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'downloaded_by');
|
||||
}
|
||||
}
|
||||
25
app/Models/ApkVersion.php
Normal file
25
app/Models/ApkVersion.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApkVersion extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'file_name',
|
||||
'uploaded_by',
|
||||
'changelog',
|
||||
'path',
|
||||
];
|
||||
|
||||
public function uploader()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by');
|
||||
}
|
||||
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(ApkLog::class, 'apk_version_id');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('apk_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('file_name');
|
||||
$table->string('path')->nullable();
|
||||
$table->text('changelog')->nullable();
|
||||
$table->foreignId('uploaded_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('apk_versions');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('apk_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('apk_version_id')->constrained('apk_versions')->onDelete('cascade');
|
||||
$table->foreignId('downloaded_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('apk_logs');
|
||||
}
|
||||
};
|
||||
@ -55,7 +55,6 @@ public function up(): void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// No se puede revertir la eliminación de datos sin un backup
|
||||
// Si necesitas revertir, restaura desde un backup de la base de datos
|
||||
//
|
||||
}
|
||||
};
|
||||
@ -11,6 +11,7 @@
|
||||
use Illuminate\Database\Seeder;
|
||||
use Notsoweb\LaravelCore\Traits\MySql\RolePermission;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
/**
|
||||
* Roles y permisos
|
||||
@ -53,7 +54,9 @@ public function run(): void
|
||||
// === APK ===
|
||||
$apk = PermissionType::updateOrCreate(['name' => 'App Móvil']);
|
||||
|
||||
$apkUpload = $this->onPermission('apk.upload', 'Subir APK de la aplicación móvil', $apk, 'api');
|
||||
$apkIndex = $this->onPermission('apk.index', 'Historial de registros apk', $apk, 'api');
|
||||
$apkEdit = $this->onPermission('apk.edit', 'Actualizar registro de apk', $apk, 'api');
|
||||
$apkDestroy = $this->onPermission('apk.destroy', 'Eliminar registro de apk', $apk, 'api');
|
||||
$apkDownload = $this->onPermission('apk.download', 'Descargar APK de la aplicación móvil', $apk, 'api');
|
||||
|
||||
// === MÓDULOS ===
|
||||
@ -190,7 +193,7 @@ public function run(): void
|
||||
// Constancias
|
||||
$tagIndex, $tagCreate, $tagEdit, $tagDestroy,
|
||||
//app
|
||||
$apkUpload, $apkDownload,
|
||||
$apkIndex, $apkEdit, $apkDestroy, $apkDownload,
|
||||
);
|
||||
|
||||
// Encargado
|
||||
|
||||
@ -61,8 +61,8 @@ php artisan storage:link --force || true
|
||||
echo "Ejecutando configuración de desarrollo..."
|
||||
composer run env:dev
|
||||
|
||||
echo "Creando directorio de claves Passport..."
|
||||
mkdir -p storage/app/keys
|
||||
echo "Creando directorios necesarios..."
|
||||
mkdir -p storage/app/keys storage/app/public/apk
|
||||
|
||||
echo "Generando claves de Passport..."
|
||||
php artisan passport:keys --force || true
|
||||
|
||||
@ -71,8 +71,8 @@ php artisan view:cache
|
||||
echo "Generando caché de eventos..."
|
||||
php artisan event:cache
|
||||
|
||||
echo "Creando directorio de claves Passport..."
|
||||
mkdir -p storage/app/keys
|
||||
echo "Creando directorios necesarios..."
|
||||
mkdir -p storage/app/keys storage/app/public/apk
|
||||
|
||||
if [ ! -f "storage/app/keys/oauth-private.key" ] || [ ! -f "storage/app/keys/oauth-public.key" ]; then
|
||||
echo "Claves de Passport no encontradas, generando..."
|
||||
|
||||
@ -94,8 +94,11 @@
|
||||
Route::post('repuve-credentials/decrypt', [SettingsController::class, 'decrypt']);
|
||||
|
||||
// Rutas App móvil
|
||||
Route::get('app', [AppController::class, 'index']);
|
||||
Route::post('app', [AppController::class, 'store']);
|
||||
Route::put('app/{app}', [AppController::class, 'update']);
|
||||
Route::delete('app/{app}', [AppController::class, 'destroy']);
|
||||
Route::get('app/download', [AppController::class, 'download']);
|
||||
Route::post('app/upload', [AppController::class, 'upload']);
|
||||
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user