CSN CDN Online

Upload once.
Serve everywhere, fast.

Send a file to the API, get back a public URL. Every file is cached for a full year at the edge.

POST https://cdn.csndev.com/upload
How a request travels
◧
Client
⬡
Edge cache
▣
Origin
Cache hit — served instantly, origin untouched Cache miss — first request goes to origin

Upload a file

Send a multipart/form-data POST to /upload with your API key. Add an optional folder field (products/categories); missing folders are created automatically.

<?php

$ch = curl_init('https://cdn.csndev.com/upload');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
    'file'   => new CURLFile($filePath, mime_content_type($filePath), basename($filePath)),
    'folder' => 'products/categories', // optional, auto-created
]);

$data = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($code === 200 && $data['success']) {
    echo $data['url']; // https://cdn.csndev.com/files/products/categories/<uuid>.jpg
} else {
    echo 'Upload failed: ' . ($data['error'] ?? 'Unknown error');
}
async function uploadFile(file, folder = '', apiKey) {
  const formData = new FormData();
  formData.append('file', file);
  if (folder) formData.append('folder', folder); // optional

  const res = await fetch('https://cdn.csndev.com/upload', {
    method: 'POST',
    headers: { 'X-API-Key': apiKey }, // never hardcode the key in public pages
    body: formData,
  });

  const data = await res.json();
  if (!data.success) throw new Error(data.error || 'Upload failed');
  return data.url;
}
# Upload
curl -X POST https://cdn.csndev.com/upload \
  -H "X-API-Key: your-secret-upload-key" \
  -F "file=@photo.jpg" \
  -F "folder=products/categories"

# Delete (by path or full URL)
curl -X DELETE https://cdn.csndev.com/delete \
  -H "X-API-Key: your-secret-upload-key" \
  -H "Content-Type: application/json" \
  -d '{"file":"products/categories/<filename>"}'
Get / share a file

Public files need no auth. Folder prefixes are kept.

https://cdn.csndev.com/files/products/categories/<filename>
Delete a file

API key required. Works with a path or a full URL.

DELETE /delete  {"file": "..."}

Laravel integration

Five steps. After this, every upload and delete in your app goes through MediaRepository, to the CDN or local disk depending on .env.

1Add CDN variables to .env

Set MEDIA_CDN_ENABLED=false to keep using local public storage.

.env
MEDIA_CDN_ENABLED=true
MEDIA_CDN_URL="https://cdn.csndev.com/upload"
MEDIA_CDN_KEY="your-cdn-secret-key"
Keep the key on the server only. Never expose it to the frontend.

2Register the service config

config/services.php
'media_cdn' => [
    'enabled' => env('MEDIA_CDN_ENABLED', false),
    'url'     => env('MEDIA_CDN_URL'),
    'key'     => env('MEDIA_CDN_KEY'),
],

Run php artisan config:clear after editing.

3Add the thumbnail accessor

Put this in the User model or any model that has a media thumbnail. CDN URLs are used as-is; older local files still resolve through Storage.

app/Models/User.php
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\Storage;

public function thumbnail(): Attribute
{
    $source = asset('default/profile.png');
    $media  = $this->media;

    if ($media && (
        str_starts_with($media->src, 'http://') ||
        str_starts_with($media->src, 'https://')
    )) {
        $source = $media->src;                    // CDN file
    } elseif ($media && Storage::exists($media->src)) {
        $source = Storage::url($media->src);      // legacy local file
    }

    return Attribute::make(get: fn () => $source);
}

4Update MediaRepository

One global place for upload, replace and delete. The new optional $folder argument is created on the CDN automatically (and used as a sub-folder for local storage). Old files are removed on replace and delete.

app/Repositories/MediaRepository.php
<?php

namespace App\Repositories;

use App\Models\Media;
use Arafat\LaravelRepository\Repository;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class MediaRepository extends Repository
{
    public static function model()
    {
        return Media::class;
    }

    /**
     * Store an uploaded file (CDN or local) with an optional folder.
     */
    public static function storeByRequest(
        UploadedFile $file,
        string $path,
        ?string $type = null,
        ?string $folder = null
    ): Media {
        self::assertCdnConfigured();

        $src = self::putFile($file, $path, $folder);

        return self::create([
            'type'      => self::resolveType($file, $type),
            'src'       => $src,
            'name'      => $file->getClientOriginalName(),
            'extension' => $file->extension(),
            'mime_type' => $file->getMimeType(),
            'file_size' => $file->getSize(),
        ]);
    }

    /**
     * Replace the file of an existing media record.
     */
    public static function updateByRequest(
        UploadedFile $file,
        string $path,
        ?string $type,
        Media $media,
        ?string $folder = null
    ): Media {
        self::assertCdnConfigured();

        $oldSrc = (string) $media->src;
        $newSrc = self::putFile($file, $path, $folder);

        $media->update([
            'type'      => self::resolveType($file, $type),
            'name'      => $file->getClientOriginalName(),
            'src'       => $newSrc,
            'extension' => $file->extension(),
            'mime_type' => $file->getMimeType(),
            'file_size' => $file->getSize(),
        ]);

        self::removeFile($oldSrc); // only after the new file is safe

        return $media;
    }

    /**
     * Delete the stored file and the media record.
     */
    public static function deleteByMedia(Media $media): bool
    {
        self::removeFile((string) $media->src);

        return (bool) $media->delete();
    }

    /* ---------------------------------------------------------------- */

    private static function putFile(UploadedFile $file, string $path, ?string $folder): string
    {
        $folder = self::cleanFolder($folder);

        if (self::useCdn()) {
            return self::uploadToCdn($file, $folder)['url'];
        }

        return Storage::disk('public')->put(
            trim($path . '/' . $folder, '/'),
            $file
        );
    }

    private static function removeFile(string $src): void
    {
        if ($src === '') {
            return;
        }

        if (str_starts_with($src, 'http://') || str_starts_with($src, 'https://')) {
            // CDN file: send only the path, not the full URL
            $file = self::useCdn() ? self::cdnPath($src) : null;

            if ($file) {
                try {
                    Http::timeout(30)
                        ->withHeaders(['X-API-Key' => config('services.media_cdn.key')])
                        ->delete(self::cdnDeleteUrl(), ['file' => $file]);
                } catch (\Throwable $e) {
                    report($e); // never block the request if CDN delete fails
                }
            }
            return;
        }

        if (Storage::disk('public')->exists($src)) {
            Storage::disk('public')->delete($src);
        }
    }

    private static function uploadToCdn(UploadedFile $file, string $folder = ''): array
    {
        $response = Http::timeout(60)
            ->withHeaders(['X-API-Key' => config('services.media_cdn.key')])
            ->attach('file', fopen($file->getRealPath(), 'r'), $file->getClientOriginalName())
            ->post(config('services.media_cdn.url'), ['folder' => $folder]);

        $data = $response->json();

        if (! $response->successful() || ! is_array($data) || empty($data['success']) || empty($data['url'])) {
            throw new \RuntimeException('CDN file upload failed: ' . ($data['error'] ?? 'Unknown CDN error'));
        }

        return $data;
    }

    /** "products/categories" style only; blocks "..", spaces, symbols. */
    private static function cleanFolder(?string $folder): string
    {
        $folder = trim((string) $folder, '/');

        if ($folder === '') {
            return '';
        }

        if (! preg_match('/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/', $folder) || str_contains($folder, '..')) {
            throw new \InvalidArgumentException('Invalid CDN folder name.');
        }

        return $folder;
    }

    /**
     * https://cdn.csndev.com/files/products/cat/uuid.jpg  =>  products/cat/uuid.jpg
     * Returns null if the URL does not belong to our CDN domain.
     */
    private static function cdnPath(string $src): ?string
    {
        $base = parse_url((string) config('services.media_cdn.url'));
        $file = parse_url($src);

        if (($file['host'] ?? null) !== ($base['host'] ?? null)) {
            return null;
        }

        $path = preg_replace('#^/files/#', '', $file['path'] ?? '');

        return ltrim($path, '/') ?: null;
    }

    private static function cdnDeleteUrl(): string
    {
        return preg_replace('#/upload/?$#', '/delete', config('services.media_cdn.url'));
    }

    private static function resolveType(UploadedFile $file, ?string $type): string
    {
        if ($type) {
            return $type;
        }

        $ext = $file->extension();

        return in_array(strtolower($ext), ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']) ? 'image' : $ext;
    }

    private static function useCdn(): bool
    {
        return (bool) config('services.media_cdn.enabled');
    }

    private static function assertCdnConfigured(): void
    {
        if (self::useCdn() && (blank(config('services.media_cdn.url')) || blank(config('services.media_cdn.key')))) {
            throw new \RuntimeException('CDN upload is enabled but not configured properly.');
        }
    }
}

5Call it from a controller

app/Http/Controllers/ProfileController.php
use App\Repositories\MediaRepository;

// Upload into a folder
$media = MediaRepository::storeByRequest(
    $request->file('photo'),
    'users',               // local fallback path
    null,                  // type (auto-detected)
    'users/avatars'        // CDN folder, auto-created
);
$user->update(['media_id' => $media->id]);

// Replace
MediaRepository::updateByRequest($request->file('photo'), 'users', null, $user->media, 'users/avatars');

// Delete file + record
MediaRepository::deleteByMedia($user->media);

// In Blade
<img src="{{ $user->thumbnail }}" alt="{{ $user->name }}">
Validate uploads before calling the repository, e.g. 'photo' => 'required|image|max:4096'.

What this service does

Store

Each upload gets a unique filename, optionally inside a folder created on demand, and is written to disk once.

Cache

Responses are marked immutable for a year, so browsers and proxies stop re-asking for them.

Serve

Files stream straight from disk with range support, so video and audio can seek instantly.