deployer fixes
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DownloadProduct;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
final class DownloadProductController extends Controller {
|
||||
public function index() { return view('admin.download-products.index', ['products'=>DownloadProduct::latest()->get()]); }
|
||||
public function store(Request $request) {
|
||||
$data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'required|file|max:512000','is_published'=>'sometimes|boolean']);
|
||||
$file=$request->file('package');
|
||||
$path=$file->store('download-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'=>$request->boolean('is_published')]);
|
||||
return back()->with('status','Product added.');
|
||||
}
|
||||
public function update(Request $request, DownloadProduct $product) {
|
||||
$data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'nullable|file|max:512000','is_published'=>'sometimes|boolean']);
|
||||
$changes=['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'is_published'=>$request->boolean('is_published')];
|
||||
if ($request->hasFile('package')) { $file=$request->file('package'); $path=$file->store('download-products','local'); $old=$product->file_path; $changes+=['file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize()]; $product->update($changes); if($old) Storage::disk('local')->delete($old); }
|
||||
else $product->update($changes);
|
||||
return back()->with('status','Product updated.');
|
||||
}
|
||||
public function destroy(DownloadProduct $product) { if($product->file_path) Storage::disk('local')->delete($product->file_path); $product->delete(); return back()->with('status','Product deleted.'); }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\DownloadProduct;
|
||||
use App\Models\SonarDeskRelease;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -16,8 +17,9 @@ final class ReleaseController extends Controller
|
||||
?? SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->orderByDesc('id')->first();
|
||||
$releases = SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);
|
||||
$settings = $this->settings();
|
||||
$products = DownloadProduct::where('is_published', true)->whereNotNull('file_path')->orderBy('name')->get();
|
||||
|
||||
return view('downloads.index', compact('latest', 'releases', 'settings'));
|
||||
return view('downloads.index', compact('latest', 'releases', 'settings', 'products'));
|
||||
}
|
||||
|
||||
public function latest(): BinaryFileResponse
|
||||
@@ -32,6 +34,13 @@ final class ReleaseController extends Controller
|
||||
return $this->send($release);
|
||||
}
|
||||
|
||||
public function productDownload(DownloadProduct $product): BinaryFileResponse
|
||||
{
|
||||
abort_unless($product->is_published && $product->file_path && Storage::disk('local')->exists($product->file_path), 404);
|
||||
$product->increment('download_count');
|
||||
return response()->download(Storage::disk('local')->path($product->file_path), $product->original_filename);
|
||||
}
|
||||
|
||||
public function appcast(): Response
|
||||
{
|
||||
$releases = SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->limit(20)->get();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DownloadProduct extends Model {
|
||||
protected $fillable = ['name','description','version','platform','file_path','original_filename','file_size','download_count','is_published'];
|
||||
protected function casts(): array { return ['is_published'=>'boolean','file_size'=>'integer','download_count'=>'integer']; }
|
||||
public function getFormattedSizeAttribute(): string { return $this->file_size >= 1048576 ? number_format($this->file_size/1048576,1).' MB' : number_format($this->file_size/1024,1).' KB'; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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 {
|
||||
Schema::create('download_products', function (Blueprint $table) {
|
||||
$table->id(); $table->string('name'); $table->text('description')->nullable();
|
||||
$table->string('version')->nullable(); $table->string('platform')->nullable();
|
||||
$table->string('file_path')->nullable(); $table->string('original_filename')->nullable();
|
||||
$table->unsignedBigInteger('file_size')->default(0); $table->unsignedBigInteger('download_count')->default(0);
|
||||
$table->boolean('is_published')->default(false); $table->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void { Schema::dropIfExists('download_products'); }
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
@extends('layouts.app', ['title' => 'Download products — Admin'])
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Distribution</span><h1>Other download products</h1><p class="lead compact">Add named products and files to the public downloads area.</p></div><a class="btn btn-secondary" href="{{ route('admin.releases.index') }}">Back to releases</a></section>
|
||||
@if(session('status'))<div class="card"><p>{{ session('status') }}</p></div>@endif
|
||||
@if($errors->any())<div class="card"><ul>@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
|
||||
<div class="editor-card"><h2>Add product</h2><form method="post" action="{{ route('admin.download-products.store') }}" enctype="multipart/form-data">@csrf
|
||||
<label>Product name<input name="name" required maxlength="150"></label><label>Description<textarea name="description" rows="3"></textarea></label><label>Version<input name="version"></label><label>Platform<input name="platform" placeholder="macOS, Windows, Linux…"></label><label>Download file<input type="file" name="package" required></label><label><input type="checkbox" name="is_published" value="1"> Publish immediately</label><p><button class="btn btn-primary">Add product</button></p></form></div>
|
||||
@foreach($products as $product)
|
||||
<div class="editor-card" style="margin-top:20px"><h2>{{ $product->name }}</h2><p>{{ $product->download_count }} downloads · {{ $product->formatted_size }} · {{ $product->is_published ? 'Published' : 'Draft' }}</p><form method="post" action="{{ route('admin.download-products.update',$product) }}" enctype="multipart/form-data">@csrf @method('PUT')
|
||||
<label>Product name<input name="name" value="{{ $product->name }}" required></label><label>Description<textarea name="description" rows="3">{{ $product->description }}</textarea></label><label>Version<input name="version" value="{{ $product->version }}"></label><label>Platform<input name="platform" value="{{ $product->platform }}"></label><label>Replace file (optional)<input type="file" name="package"></label><label><input type="checkbox" name="is_published" value="1" @checked($product->is_published)> Published</label><p><button class="btn btn-primary">Save changes</button></p></form><form method="post" action="{{ route('admin.download-products.destroy',$product) }}" onsubmit="return confirm('Delete this product and its file?')">@csrf @method('DELETE')<button class="danger-outline">Delete product</button></form></div>
|
||||
@endforeach
|
||||
@endsection
|
||||
@@ -1,6 +1,6 @@
|
||||
@extends('layouts.app', ['title' => 'Releases — SonarDesk Admin'])
|
||||
@section('content')
|
||||
<section class="portal-header admin-release-head"><div><span class="eyebrow">Distribution</span><h1>Release management</h1><p class="lead compact">Publish SonarDesk builds, manage the public download experience and keep Sparkle updates under control.</p></div><div class="support-actions"><a class="btn btn-secondary" href="{{ route('admin.releases.settings') }}">Download page settings</a><a class="btn btn-primary" href="{{ route('admin.releases.create') }}">New release</a></div></section>
|
||||
<section class="portal-header admin-release-head"><div><span class="eyebrow">Distribution</span><h1>Release management</h1><p class="lead compact">Publish SonarDesk builds, manage the public download experience and keep Sparkle updates under control.</p></div><div class="support-actions"><a class="btn btn-secondary" href="{{ route('admin.download-products.index') }}">Other products</a><a class="btn btn-secondary" href="{{ route('admin.releases.settings') }}">Download page settings</a><a class="btn btn-primary" href="{{ route('admin.releases.create') }}">New release</a></div></section>
|
||||
<div class="grid admin-metrics"><div class="card"><strong>{{ $releases->total() }}</strong><p>Total releases</p></div><div class="card"><strong>{{ $publishedCount }}</strong><p>Published</p></div><div class="card"><strong>{{ number_format($downloadCount) }}</strong><p>Total downloads</p></div><div class="card"><strong>Live</strong><p>Sparkle feed</p></div></div>
|
||||
<div class="admin-table-card"><div class="admin-table-title"><div><h2>SonarDesk builds</h2><p>Only published builds with an uploaded package are available publicly.</p></div><a href="{{ route('release') }}" class="btn btn-secondary" target="_blank">View public page</a></div><div class="table-scroll"><table class="pro-table"><thead><tr><th>Release</th><th>Channel</th><th>Status</th><th>Package</th><th>Downloads</th><th>Published</th><th></th></tr></thead><tbody>@forelse($releases as $release)<tr><td><strong>SonarDesk {{ $release->version }}</strong><small>Build {{ $release->build_number }} · macOS {{ $release->minimum_macos ?: 'Any supported' }}</small></td><td><span class="status-chip">{{ ucfirst($release->channel) }}</span></td><td>@if($release->is_published)<span class="status-chip success">Published</span>@else<span class="status-chip muted">Draft</span>@endif</td><td>{{ $release->file_path ? $release->formatted_size : 'No package' }}</td><td>{{ number_format($release->download_count) }}</td><td>{{ $release->published_at?->format('j M Y H:i') ?: '—' }}</td><td class="table-actions"><a class="btn btn-secondary" href="{{ route('admin.releases.edit',$release) }}">Edit</a><form method="post" action="{{ route('admin.releases.destroy',$release) }}" onsubmit="return confirm('Delete this release and its package?')">@csrf @method('DELETE')<button class="danger-outline" type="submit">Delete</button></form></td></tr>@empty<tr><td colspan="7" class="empty-cell">No releases yet. Create your first signed SonarDesk build.</td></tr>@endforelse</tbody></table></div></div>
|
||||
{{ $releases->links() }}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<main class="download-page"><section class="download-hero"><div class="download-shell"><div class="download-copy"><span class="eyebrow">{{ $settings['eyebrow'] }}</span><h1>{{ $settings['title'] }}</h1><p>{{ $settings['intro'] }}</p>@if($latest)<div class="download-actions"><a class="btn download-primary" href="{{ route('download.latest') }}"><span class="download-icon">↓</span><span><strong>Download SonarDesk {{ $latest->version }}</strong><small>{{ $latest->formatted_size }} · macOS {{ $latest->minimum_macos ?: 'supported versions' }}</small></span></a><a class="btn download-secondary" href="#release-notes">View release notes</a></div><div class="release-verification"><span>✓ Signed distribution</span><span>✓ SHA-256 verified</span><span>✓ Secure licence activation</span></div>@else<div class="no-release"><strong>The next SonarDesk release is being prepared.</strong><span>Check back shortly or contact support.</span></div>@endif</div><div class="product-visual"><div class="mac-window"><div class="mac-titlebar"><i></i><i></i><i></i><span>SonarDesk</span></div><div class="mac-content"><div class="mini-sidebar"><img src="{{ asset('icons/sonardesk-icon-64.png') }}" alt=""><b>SonarDesk</b><span class="active"></span><span></span><span></span><span></span><span></span></div><div class="mini-dashboard"><div class="mini-head"><b>Project health</b><em>All systems monitored</em></div><div class="mini-stats"><i></i><i></i><i></i><i></i></div><div class="mini-chart"><span></span><span></span><span></span><span></span><span></span><span></span></div></div></div></div><div class="visual-badge"><span>Native</span><strong>Built for macOS</strong></div></div></div></section>
|
||||
<section class="download-confidence"><div class="download-shell confidence-grid"><div><span>Native macOS</span><strong>Fast, focused desktop workflow</strong></div><div><span>SonarQube</span><strong>Your server remains the source of truth</strong></div><div><span>Security</span><strong>Tokens protected by macOS Keychain</strong></div><div><span>Updates</span><strong>Professional in-app release delivery</strong></div></div></section>
|
||||
<div class="download-shell download-content"><section id="release-notes" class="release-notes-panel">@if($latest)<div class="release-heading"><div><span class="eyebrow">Latest stable release</span><h2>SonarDesk {{ $latest->version }}</h2><p>Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}</p></div><a class="btn btn-primary" href="{{ route('download.release',$latest) }}">Download {{ $latest->version }}</a></div><div class="release-note-copy">{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}</div><div class="checksum-box"><span>SHA-256</span><code>{{ $latest->sha256 }}</code></div>@endif</section><aside class="requirements-panel"><span class="eyebrow">Before you install</span><h2>System requirements</h2><p>{{ $settings['requirements'] }}</p><ol><li><span>1</span><div><strong>Download</strong><small>Get the latest signed SonarDesk package.</small></div></li><li><span>2</span><div><strong>Install</strong><small>Open the DMG and move SonarDesk to Applications.</small></div></li><li><span>3</span><div><strong>Activate</strong><small>Enter the licence key delivered after purchase.</small></div></li></ol><a href="{{ route('buy') }}">Need a licence? View pricing →</a></aside></div>
|
||||
@if($products->isNotEmpty())
|
||||
<section class="release-history"><div class="download-shell"><div class="section-heading"><span class="eyebrow">More downloads</span><h2>Other products</h2><p>Explore our other available software and downloads.</p></div><div class="release-history-list">@foreach($products as $product)<article><div><strong>{{ $product->name }}@if($product->version) {{ $product->version }}@endif</strong><span>{{ $product->description }}</span>@if($product->platform)<span>{{ $product->platform }}</span>@endif</div><div><span>{{ $product->formatted_size }}</span><a href="{{ route('download.product',$product) }}">Download</a></div></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
<section class="release-history"><div class="download-shell"><div class="section-heading"><span class="eyebrow">Release archive</span><h2>Previous versions</h2><p>Published SonarDesk builds remain available here for customers who need a previous release.</p></div><div class="release-history-list">@forelse($releases as $release)<article><div><strong>SonarDesk {{ $release->version }}</strong><span>Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}</span></div><div><span>{{ $release->formatted_size }}</span><a href="{{ route('download.release',$release) }}">Download</a></div></article>@empty<p>No published releases are available yet.</p>@endforelse</div>{{ $releases->links() }}</div></section>
|
||||
<section class="download-support"><div class="download-shell"><div><span class="eyebrow">Support</span><h2>Installation or activation help?</h2><p>{{ $settings['support_text'] }}</p></div><a class="btn btn-secondary" href="{{ route('support') }}">Contact SonarDesk support</a></div></section></main>
|
||||
@endsection
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
<a href="{{ route('admin.index') }}">Admin</a>
|
||||
<a href="{{ route('admin.users.index') }}">Users</a>
|
||||
<a href="{{ route('admin.releases.index') }}">Releases</a>
|
||||
<a href="{{ route('admin.download-products.index') }}">Download products</a>
|
||||
<a href="{{ route('admin.support.index') }}">
|
||||
Support
|
||||
@if ($supportUnread > 0)
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Http\Controllers\Auth\EmailVerificationController;
|
||||
use App\Http\Controllers\Auth\PasswordController;
|
||||
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||
use App\Http\Controllers\ReleaseController;
|
||||
use App\Http\Controllers\Admin\DownloadProductController;
|
||||
use App\Http\Controllers\Support\SupportController;
|
||||
use App\Http\Middleware\AdminOnly;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -25,6 +26,7 @@ Route::get('/', fn () => view('portal.welcome', ['latestRelease' => \App\Models\
|
||||
Route::view('/features', 'features')->name('features');
|
||||
Route::get('/release', [ReleaseController::class, 'index'])->name('release');
|
||||
Route::get('/download', [ReleaseController::class, 'latest'])->name('download.latest');
|
||||
Route::get('/downloads/products/{product}', [ReleaseController::class, 'productDownload'])->name('download.product');
|
||||
Route::get('/downloads/{release}', [ReleaseController::class, 'download'])->name('download.release');
|
||||
Route::get('/updates/appcast.xml', [ReleaseController::class, 'appcast'])->name('updates.appcast');
|
||||
Route::get('/support', [SupportController::class, 'create'])->name('support');
|
||||
@@ -74,6 +76,10 @@ Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name
|
||||
Route::get('/cms/{page}/edit', [AdminCmsPageController::class, 'edit'])->name('cms.edit');
|
||||
Route::put('/cms/{page}', [AdminCmsPageController::class, 'update'])->name('cms.update');
|
||||
Route::delete('/cms/{page}', [AdminCmsPageController::class, 'destroy'])->name('cms.destroy');
|
||||
Route::get('/download-products', [DownloadProductController::class, 'index'])->name('download-products.index');
|
||||
Route::post('/download-products', [DownloadProductController::class, 'store'])->name('download-products.store');
|
||||
Route::put('/download-products/{product}', [DownloadProductController::class, 'update'])->name('download-products.update');
|
||||
Route::delete('/download-products/{product}', [DownloadProductController::class, 'destroy'])->name('download-products.destroy');
|
||||
Route::get('/releases', [AdminReleaseController::class, 'index'])->name('releases.index');
|
||||
Route::get('/releases/create', [AdminReleaseController::class, 'create'])->name('releases.create');
|
||||
Route::post('/releases', [AdminReleaseController::class, 'store'])->name('releases.store');
|
||||
|
||||
Reference in New Issue
Block a user