move into sonardesk
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration {
|
||||
public function up(): void {
|
||||
if (!Schema::hasTable('sonardesk_releases')) Schema::create('sonardesk_releases', function(Blueprint $t) {
|
||||
$t->id(); $t->string('version',40); $t->unsignedInteger('build_number')->default(1); $t->string('channel',20)->default('stable'); $t->string('minimum_macos',30)->nullable(); $t->string('file_path')->nullable(); $t->string('original_filename')->nullable(); $t->unsignedBigInteger('file_size')->nullable(); $t->string('sha256',64)->nullable(); $t->text('sparkle_signature')->nullable(); $t->text('release_notes')->nullable(); $t->boolean('is_published')->default(false)->index(); $t->boolean('is_featured')->default(false)->index(); $t->timestamp('published_at')->nullable()->index(); $t->unsignedBigInteger('download_count')->default(0); $t->timestamps(); $t->unique(['version','build_number','channel']);
|
||||
});
|
||||
if (!Schema::hasTable('download_products')) Schema::create('download_products',function(Blueprint $t){$t->id();$t->string('name');$t->text('description')->nullable();$t->string('version')->nullable();$t->string('platform')->nullable();$t->string('file_path')->nullable();$t->string('original_filename')->nullable();$t->unsignedBigInteger('file_size')->default(0);$t->unsignedBigInteger('download_count')->default(0);$t->boolean('is_published')->default(false);$t->timestamps();});
|
||||
}
|
||||
public function down(): void { /* Preserve release and download data on rollback; use an explicit data migration to remove. */ }
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Licensing\Http\Controllers\Admin;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Licensing\Models\{SonarDeskRelease,DownloadProduct};
|
||||
final class DistributionAdminController extends Controller {
|
||||
public function index(){return view('licensing::distribution.admin', ['releases'=>SonarDeskRelease::orderByDesc('created_at')->paginate(20),'products'=>DownloadProduct::orderBy('name')->get()]);}
|
||||
public function release(Request $request){$data=$request->validate(['version'=>'required|string|max:40','build_number'=>'required|integer|min:1','channel'=>['required',Rule::in(['stable','beta'])],'minimum_macos'=>'nullable|string|max:30','release_notes'=>'nullable|string','sparkle_signature'=>'nullable|string|max:2048','is_published'=>'nullable|boolean','is_featured'=>'nullable|boolean','file'=>'required|file|max:512000|mimes:dmg,zip,pkg']);$file=$request->file('file');$path=$file->store('sonardesk-distribution/releases','local');$release=SonarDeskRelease::create(['version'=>$data['version'],'build_number'=>$data['build_number'],'channel'=>$data['channel'],'minimum_macos'=>$data['minimum_macos']??null,'release_notes'=>$data['release_notes']??null,'sparkle_signature'=>$data['sparkle_signature']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'sha256'=>hash_file('sha256',$file->getRealPath()),'is_published'=>(bool)($data['is_published']??false),'is_featured'=>(bool)($data['is_featured']??false),'published_at'=>!empty($data['is_published'])?now():null]);return redirect()->route('administration.sonardesk.distribution')->with('status','Release '.$release->version.' created');}
|
||||
public function product(Request $request){$data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string','version'=>'nullable|string|max:60','platform'=>'nullable|string|max:60','is_published'=>'nullable|boolean','file'=>'required|file|max:512000']);$file=$request->file('file');$path=$file->store('sonardesk-distribution/products','local');DownloadProduct::create(['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'is_published'=>(bool)($data['is_published']??false)]);return redirect()->route('administration.sonardesk.distribution')->with('status','Download created');}
|
||||
public function publish(Request $request,SonarDeskRelease $release){$data=$request->validate(['is_published'=>'required|boolean']);$release->update(['is_published'=>(bool)$data['is_published'],'published_at'=>$data['is_published']?($release->published_at??now()):null]);return back()->with('status','Release status updated');}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Modules\Licensing\Http\Controllers;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Licensing\Models\{SonarDeskRelease,DownloadProduct};
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
final class DistributionController extends Controller {
|
||||
public function index(){ $latest=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->first(); $releases=SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);$products=DownloadProduct::where('is_published',true)->whereNotNull('file_path')->orderBy('name')->get();return view('licensing::downloads.index',compact('latest','releases','products')); }
|
||||
public function release(SonarDeskRelease $release){abort_unless($release->is_published && $release->published_at && $release->file_path,404);return view('licensing::downloads.release',compact('release'));}
|
||||
public function product(DownloadProduct $product){abort_unless($product->is_published && $product->file_path,404);return view('licensing::downloads.product',compact('product'));}
|
||||
public function latest():BinaryFileResponse{$r=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->firstOrFail();return $this->send($r);}
|
||||
public function download(SonarDeskRelease $release):BinaryFileResponse{abort_unless($release->is_published && $release->published_at,404);return $this->send($release);}
|
||||
public function productDownload(DownloadProduct $product):BinaryFileResponse{abort_unless($product->is_published,404);$path=$this->safePath($product->file_path);$product->increment('download_count');return response()->download($path,$product->original_filename ?: basename($path));}
|
||||
private function safePath(?string $relative):string {abort_unless($relative && preg_match('~^sonardesk-distribution/[a-zA-Z0-9_./-]+$~D',$relative) && !str_contains($relative,'..'),404);$disk=Storage::disk('local');abort_unless($disk->exists($relative),404);$base=realpath($disk->path('sonardesk-distribution'));$file=realpath($disk->path($relative));abort_unless($base && $file && str_starts_with($file,$base.DIRECTORY_SEPARATOR),404);return $file;}
|
||||
private function send(SonarDeskRelease $r):BinaryFileResponse{$path=$this->safePath($r->file_path);$r->increment('download_count');return response()->download($path,$r->original_filename ?: 'SonarDesk-'.$r->version.'.dmg');}
|
||||
public function appcast(){ $releases=SonarDeskRelease::published()->where('channel','stable')->whereNotNull('sparkle_signature')->orderByDesc('published_at')->limit(20)->get();return response()->view('licensing::downloads.appcast',compact('releases'),200,['Content-Type'=>'application/rss+xml; charset=UTF-8','Cache-Control'=>'public, max-age=300']); }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Modules\Licensing\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DownloadProduct extends Model {
|
||||
protected $table='download_products';
|
||||
protected $fillable=['name','description','version','platform','file_path','original_filename','file_size','is_published'];
|
||||
protected function casts():array{return ['is_published'=>'boolean','file_size'=>'integer'];}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace Modules\Licensing\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class SonarDeskRelease extends Model {
|
||||
protected $table='sonardesk_releases';
|
||||
protected $fillable=['version','build_number','channel','minimum_macos','file_path','original_filename','file_size','sha256','sparkle_signature','release_notes','is_published','is_featured','published_at'];
|
||||
protected function casts():array{return ['is_published'=>'boolean','is_featured'=>'boolean','published_at'=>'datetime','file_size'=>'integer','build_number'=>'integer'];}
|
||||
public function scopePublished($query){return $query->where('is_published',true)->whereNotNull('file_path')->whereNotNull('published_at');}
|
||||
}
|
||||
@@ -33,5 +33,6 @@ final class IntegratedLicensingServiceProvider extends ServiceProvider
|
||||
Route::prefix('api')->middleware('api')->group(__DIR__.'/../Routes/api.php');
|
||||
Route::group([], __DIR__.'/../Routes/admin.php');
|
||||
Route::group([], __DIR__.'/../Routes/customer.php');
|
||||
Route::group([], __DIR__.'/../Routes/distribution.php');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@extends('licensing::admin.layout')
|
||||
@section('content')
|
||||
<h1>Downloads & releases</h1>@if(session('status'))<p>{{ session('status') }}</p>@endif
|
||||
<h2>Publish SonarDesk release</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.store') }}">@csrf
|
||||
<p><input name="version" placeholder="Version (e.g. 1.2.0)" required> <input name="build_number" type="number" min="1" placeholder="Build" required> <select name="channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></p>
|
||||
<p><input name="minimum_macos" placeholder="Minimum macOS version"><input type="file" name="file" accept=".dmg,.zip,.pkg" required></p><p><textarea name="release_notes" placeholder="Full release notes" rows="5" cols="70"></textarea></p><p><input name="sparkle_signature" placeholder="Sparkle EdDSA signature (optional)" size="65"></p><label><input type="checkbox" name="is_published" value="1"> Publish</label> <button type="submit">Create release</button></form>
|
||||
<h2>Releases</h2><table><thead><tr><th>Version</th><th>Channel</th><th>Status</th><th>Downloads</th><th>Action</th></tr></thead><tbody>@foreach($releases as $r)<tr><td>{{ $r->version }} ({{ $r->build_number }})</td><td>{{ $r->channel }}</td><td>{{ $r->is_published ? 'Published' : 'Draft' }}</td><td>{{ $r->download_count }}</td><td><form method="POST" action="{{ route('administration.sonardesk.distribution.releases.publish',$r) }}">@csrf @method('PATCH')<input type="hidden" name="is_published" value="{{ $r->is_published ? 0 : 1 }}"><button>{{ $r->is_published ? 'Unpublish' : 'Publish' }}</button></form></td></tr>@endforeach</tbody></table>{{ $releases->links() }}
|
||||
<h2>Add other download (e.g. SonarDisk)</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.store') }}">@csrf<p><input name="name" placeholder="Product name" required><input name="version" placeholder="Version"><input name="platform" placeholder="Platform"></p><p><textarea name="description" placeholder="Description"></textarea></p><p><input name="file" type="file" required></p><label><input type="checkbox" name="is_published" value="1"> Publish</label><button>Add download</button></form><h2>Other downloads</h2><ul>@foreach($products as $p)<li>{{ $p->name }} {{ $p->version }} — {{ $p->download_count }} downloads ({{ $p->is_published ? 'published' : 'draft' }})</li>@endforeach</ul>
|
||||
@endsection
|
||||
@@ -0,0 +1,2 @@
|
||||
@php echo '<?xml version="1.0" encoding="utf-8"?>'; @endphp
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"><channel><title>SonarDesk Updates</title><link>{{ route('sonardesk.updates.appcast') }}</link><description>SonarDesk stable release feed</description>@foreach($releases as $release)<item><title>SonarDesk {{ $release->version }}</title><pubDate>{{ $release->published_at?->toRfc2822String() }}</pubDate><sparkle:version>{{ $release->build_number }}</sparkle:version><sparkle:shortVersionString>{{ $release->version }}</sparkle:shortVersionString>@if($release->minimum_macos)<sparkle:minimumSystemVersion>{{ $release->minimum_macos }}</sparkle:minimumSystemVersion>@endif<description><![CDATA[{!! nl2br(e($release->release_notes ?: 'SonarDesk update')) !!}]]></description><enclosure url="{{ route('sonardesk.releases.download',$release) }}" length="{{ $release->file_size }}" type="application/octet-stream" sparkle:edSignature="{{ $release->sparkle_signature }}" /></item>@endforeach</channel></rss>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SonarDesk Downloads</title><style>body{font:16px system-ui;background:#f6f8fc;color:#17243c;margin:0}.wrap{max-width:1120px;margin:48px auto;padding:0 24px}h1{font-size:42px;margin-bottom:6px}.card{background:white;border:1px solid #dce3ee;border-radius:14px;padding:22px;margin:18px 0}a.button{display:inline-block;background:#183b73;color:white;padding:10px 18px;border-radius:8px;text-decoration:none}small{color:#58657a}ul{line-height:2}</style></head><body><main class="wrap"><h1>SonarDesk Downloads</h1><p>Current applications and release history.</p>@if($latest)<section class="card"><h2>SonarDesk <small>{{ $latest->version }}</small></h2><p>{{ \Illuminate\Support\Str::limit(strip_tags($latest->release_notes ?? 'Native SonarQube dashboard for macOS.'),190) }}</p><a class="button" href="{{ route('sonardesk.releases.download',$latest) }}">Download for macOS</a> <a href="{{ route('sonardesk.releases.show',$latest) }}">Release notes & requirements</a></section>@endif @if($products->isNotEmpty())<section class="card"><h2>Other downloads</h2><ul>@foreach($products as $product)<li><strong>{{ $product->name }}</strong> {{ $product->version }} ({{ $product->platform }}) — <a href="{{ route('sonardesk.products.download',$product) }}">Download</a> · <a href="{{ route('sonardesk.products.show',$product) }}">Details</a></li>@endforeach</ul></section>@endif<section class="card"><h2>Previous releases</h2><ul>@forelse($releases as $release)<li><strong>{{ $release->version }}</strong> — {{ $release->channel }} · <a href="{{ route('sonardesk.releases.show',$release) }}">Notes</a> · <a href="{{ route('sonardesk.releases.download',$release) }}">Download</a></li>@empty<li>No releases published.</li>@endforelse</ul>{{ $releases->links() }}</section></main></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ $product->name }}</title></head><body style="font:16px system-ui;max-width:900px;margin:50px auto;padding:20px"><a href="{{ route('sonardesk.downloads') }}">← All downloads</a><h1>{{ $product->name }}</h1><p>Version {{ $product->version }} · {{ $product->platform }}</p><div style="white-space:pre-wrap">{{ $product->description }}</div><p><a href="{{ route('sonardesk.products.download',$product) }}">Download {{ $product->original_filename }}</a></p></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>SonarDesk {{ $release->version }}</title></head><body style="font:16px system-ui;max-width:900px;margin:50px auto;padding:20px"><a href="{{ route('sonardesk.downloads') }}">← All downloads</a><h1>SonarDesk {{ $release->version }}</h1><p>Build {{ $release->build_number }} · {{ $release->channel }}</p><h2>Release notes</h2><div style="white-space:pre-wrap">{{ $release->release_notes ?: 'No release notes provided.' }}</div><h2>System requirements</h2><p>macOS {{ $release->minimum_macos ?: 'See application documentation' }} or later.</p><p>SHA-256: <code>{{ $release->sha256 }}</code></p><a href="{{ route('sonardesk.releases.download',$release) }}">Download {{ $release->original_filename }}</a></body></html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Licensing\Http\Controllers\DistributionController as PublicDownloads;
|
||||
use Modules\Licensing\Http\Controllers\Admin\DistributionAdminController as AdminDownloads;
|
||||
Route::middleware(['web','throttle:60,1'])->prefix('sonardesk')->name('sonardesk.')->group(function(){
|
||||
Route::get('/downloads',[PublicDownloads::class,'index'])->name('downloads');
|
||||
Route::get('/downloads/releases/{release}',[PublicDownloads::class,'release'])->name('releases.show');
|
||||
Route::get('/downloads/products/{product}',[PublicDownloads::class,'product'])->name('products.show');
|
||||
Route::get('/downloads/latest',[PublicDownloads::class,'latest'])->name('latest');
|
||||
Route::get('/downloads/releases/{release}/file',[PublicDownloads::class,'download'])->name('releases.download');
|
||||
Route::get('/downloads/products/{product}/file',[PublicDownloads::class,'productDownload'])->name('products.download');
|
||||
Route::get('/updates/appcast.xml',[PublicDownloads::class,'appcast'])->name('updates.appcast');
|
||||
});
|
||||
Route::middleware(['web','auth','verified','can:manage-sonardesk-licensing'])->prefix('administration/sonardesk')->name('administration.sonardesk.')->group(function(){
|
||||
Route::get('/distribution',[AdminDownloads::class,'index'])->name('distribution');
|
||||
Route::post('/distribution/releases',[AdminDownloads::class,'release'])->name('distribution.releases.store');
|
||||
Route::post('/distribution/products',[AdminDownloads::class,'product'])->name('distribution.products.store');
|
||||
Route::patch('/distribution/releases/{release}/publish',[AdminDownloads::class,'publish'])->name('distribution.releases.publish');
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
# Phase 11 — SonarDesk distribution
|
||||
Install patch at ABSDEV root; all paths are relative to the project root. No root-level module duplicates are included.
|
||||
|
||||
```sh
|
||||
php artisan migrate
|
||||
composer dump-autoload
|
||||
php artisan optimize:clear
|
||||
php artisan route:list --path=sonardesk
|
||||
php artisan route:list --path=administration/sonardesk/distribution
|
||||
```
|
||||
Admin: `/administration/sonardesk/distribution`; public: `/sonardesk/downloads`; feed: `/sonardesk/updates/appcast.xml`.
|
||||
|
||||
Release and download binaries are stored on the private `local` disk in `sonardesk-distribution/`. Ensure the storage directory is persistent across deployments, backed up, and writable by PHP. Do not put it in public webroot. Public download responses use the Laravel controller.
|
||||
|
||||
The database schema preserves the existing legacy `sonardesk_releases` and `download_products` column names. If those tables already exist, the migration does not recreate them. Legacy files and records are **not** automatically imported; migrate their storage paths into `sonardesk-distribution/` before enabling public downloads.
|
||||
|
||||
Sparkle appcast includes only stable published releases with a non-empty Sparkle signature. Generate signatures with the official Sparkle signing tool; SHA-256 is not a substitute. Configure the macOS client to point to this feed only after validating compatibility. Existing live appcast and checkout remain unchanged.
|
||||
|
||||
The patch includes release creation, publication toggle, separate product upload and read-only public details. Editing existing releases, deletion, detailed per-platform requirements, and automatic client-side updates are not implemented here. Uploaded files are limited to 500 MiB by Laravel validation and additionally by PHP/nginx upload limits. Review your server upload limits.
|
||||
|
||||
Do not apply to production before testing staging, storage persistence, URLs, signatures and permissions. No database data is deleted on rollback.
|
||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Content_Manager",
|
||||
"name": "ABSDEV",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
Reference in New Issue
Block a user