adapter($disk)->store($file, $purpose); return File::create([ 'disk' => $disk, 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime' => $file->getMimeType(), 'size' => $file->getSize(), 'purpose' => $purpose, ]); } /** * Re-points an existing File at its owner — called once an owner * actually exists (e.g. a shopper's picked-but-not-yet-added photo * gets a CartLine the moment it's added to the cart). Never creates a * second row for the same physical file. */ public function attachOwner(File $file, Model $owner): File { $file->update([ 'owner_type' => $owner->getMorphClass(), 'owner_id' => $owner->getKey(), ]); return $file; } public function retrieve(File $file): StreamedResponse { return $this->adapter($file->disk)->retrieve($file->path, $file->original_name); } /** * Same file as retrieve(), forced as a download (Content-Disposition: * attachment) rather than served inline — for a button distinct from * a preview link/thumbnail pointing at the same File. */ public function download(File $file): StreamedResponse { return $this->adapter($file->disk)->download($file->path, $file->original_name); } public function exists(File $file): bool { return $this->adapter($file->disk)->exists($file->path); } public function delete(File $file): void { $this->adapter($file->disk)->delete($file->path); $file->delete(); } /** * @return Collection */ public function list(string $purpose, ?Model $owner = null): Collection { return File::query() ->where('purpose', $purpose) ->when($owner, fn ($query) => $query ->where('owner_type', $owner->getMorphClass()) ->where('owner_id', $owner->getKey())) ->get(); } /** * Deletes every File of the given purpose that has no owner yet and * is older than $olderThan — the grace period covers a shopper still * on the page with a picked-but-not-yet-added file. An owned File * (whatever the owner type) is never touched here; callers that want * owned files gone too should delete() them explicitly wherever that * ownership itself ends (e.g. a CartLine being removed). * * @return int number of files deleted */ public function pruneUnowned(string $purpose, Carbon $olderThan): int { $files = File::query() ->where('purpose', $purpose) ->whereNull('owner_type') ->where('created_at', '<', $olderThan) ->get(); foreach ($files as $file) { $this->delete($file); } return $files->count(); } private function adapter(string $disk): FileAdapterInterface { return $this->container->make(FileAdapterInterface::class, ['disk' => $disk]); } }