Skip to content

Commit

Permalink
feat: image references stored to database
Browse files Browse the repository at this point in the history
  • Loading branch information
Justintime50 committed Sep 21, 2023
1 parent 70a60fe commit 4a5fe4b
Show file tree
Hide file tree
Showing 21 changed files with 389 additions and 168 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Next Release

- Image references are now stored in the database instead of losely through the filesystem. This means that tracking what images belong to what users and posts should work much smoother. Additionally, various improvements were made across the app to how images are handled resulting in a vastly improved image experience
- Categories are now unique and required, enforced at the database level
- Various database indexes were added for common lookup patterns for quick retrieval
- Foreign key constraints were added to enforce data integrity at the database level when records are related to one another
Expand Down
117 changes: 117 additions & 0 deletions src/app/Http/Controllers/ImageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace App\Http\Controllers;

use App\Models\Image;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Intervention\Image\ImageManagerStatic;

class ImageController extends Controller
{
public static $defaultBannerImage = 'pics/banner.jpg';
public static $postImagesPath = 'storage/images/posts';
public static $postImagesSubdirectory = 'posts';
public static $avatarImagesSubdirectory = 'avatars';
public static $imagesDir = 'images';

/**
* Show the image gallery.
*
* Images will have a unique ID associated with them which can be referenced to show the images in posts.
*
* @param Request $request
* @return View
*/
public function showImagesPage(Request $request): View
{
$images = Image::where('subdirectory', '=', self::$postImagesSubdirectory)
->get();

return view('images', compact('images'));
}

/**
* Upload an image to local storage.
*
* @param Request $request
* @return RedirectResponse
*/
public function uploadPostImage(Request $request): RedirectResponse
{
$request->validate([
'upload_image' => 'required|image|mimes:jpeg,jpg,png|max:2048',
]);

$file = $request->file('upload_image');
$filename = ImageController::sanatizeImageFilename($file);

ImageManagerStatic::make($file)
->save(ImageController::getImagePublicPath(self::$postImagesSubdirectory, $filename));

$image = new Image();
$image->subdirectory = self::$postImagesSubdirectory;
$image->filename = $filename;
$image->save();

session()->flash('message', 'Image uploaded successfully.');
return redirect()->back();
}

/**
* Delete an image.
*
* @param Request $request
* @param int $id
* @return RedirectResponse
*/
public function deletePostImage(Request $request, int $id): RedirectResponse
{
$image = Image::find($id);
unlink(ImageController::getImagePublicPath($image->subdirectory, $image->filename));
$image->delete();

session()->flash('message', 'Image deleted.');
return redirect()->back();
}

/**
* Sanatizes an image filename.
*
* @param \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|array|null $file
* @return string
*/
public static function sanatizeImageFilename($file): string
{
$filename = preg_replace('/[^A-Za-z0-9\-\_]/', '', pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME));
$fileExtension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION);
$newFilename = $filename . '-' . time() . '.' . $fileExtension;

return $newFilename;
}

/**
* Gets the image asset path for a post.
*
* @param ?string $subdirectory
* @param ?string $imageName
* @return string|null
*/
public static function getImageAssetPath(?string $subdirectory, ?string $imageName): string|null
{
return isset($subdirectory) && isset($imageName) ? asset('storage/' . self::$imagesDir . "/$subdirectory/$imageName") : null;
}

/**
* Gets the public image path for a post.
*
* @param ?string $subdirectory
* @param ?string $imageName
* @return string|null
*/
public static function getImagePublicPath(?string $subdirectory, ?string $imageName): string|null
{
return isset($subdirectory) && isset($imageName) ? public_path('storage/' . self::$imagesDir . "/$subdirectory/$imageName") : null;
}
}
101 changes: 19 additions & 82 deletions src/app/Http/Controllers/PostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

use App\Models\Category;
use App\Models\Comment;
use App\Models\Image;
use App\Models\Post;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use Intervention\Image\ImageManagerStatic as Image;

class PostController extends Controller
{
Expand Down Expand Up @@ -108,6 +108,7 @@ public function showPost(Request $request, string $user, string $slug): View
->where('published', '=', 1)
->firstOrFail();
}

$comments = Comment::where('post_id', '=', $post->id)
->orderBy('created_at', 'asc')
->paginate(10);
Expand All @@ -125,7 +126,10 @@ public function showCreatePage(Request $request): View
{
$categories = Category::all();

return view('create-post', compact('categories'));
$images = Image::where('subdirectory', '=', ImageController::$postImagesSubdirectory)
->get();

return view('create-post', compact('categories', 'images'));
}

/**
Expand All @@ -140,9 +144,13 @@ public function showEditPage(Request $request, string $user, string $slug): View
{
$post = Post::where('slug', '=', $slug)
->firstOrFail();

$categories = Category::all();

return view('edit-post', compact('post', 'categories'));
$images = Image::where('subdirectory', '=', ImageController::$postImagesSubdirectory)
->get();

return view('edit-post', compact('post', 'categories', 'images'));
}

/**
Expand All @@ -168,14 +176,14 @@ public function create(Request $request): RedirectResponse
'keywords' => 'nullable|string',
'category_id' => 'nullable|integer',
'post' => 'required|string',
'banner_image_url' => 'nullable|string',
'image_id' => 'nullable|integer',
'published' => 'required|integer',
]);

$post->title = $request->input('title');
$post->slug = $request->input('slug');
$post->published = $request->input('published');
$post->banner_image_url = $request->input('banner_image_url');
$post->image_id = $request->input('image_id');
$post->keywords = $request->input('keywords');
$post->category_id = $request->input('category_id');
$post->post = $request->input('post');
Expand Down Expand Up @@ -208,12 +216,12 @@ public function update(Request $request, int $id): RedirectResponse
'keywords' => 'nullable|string',
'category_id' => 'nullable|integer',
'post' => 'required|string',
'banner_image_url' => 'nullable|string',
'image_id' => 'nullable|integer',
'published' => 'required|integer',
]);

$post->published = $request->input('published');
$post->banner_image_url = $request->input('banner_image_url');
$post->image_id = $request->input('image_id');
$post->title = $request->input('title');
$post->slug = $request->input('slug');
$post->keywords = $request->input('keywords');
Expand All @@ -227,7 +235,7 @@ public function update(Request $request, int $id): RedirectResponse
}

/**
* Delete a post and its associated comments.
* Delete a post, its image, and its associated comments.
*
* @param Request $request
* @param int $id
Expand All @@ -237,64 +245,15 @@ public function delete(Request $request, int $id): RedirectResponse
{
$post = Post::find($id);
$post->comments()->delete();
$image = Image::find($post->image_id);
unlink(ImageController::getImagePublicPath($image->subdirectory, $image->filename));
$image->delete();
$post->delete();

session()->flash('message', 'Post deleted.');
return redirect('/');
}

/**
* Show the image gallery.
*
* Images will have a unique ID associated with them which can be referenced to show the images in posts.
*
* @param Request $request
* @return View
*/
public function showImagesPage(Request $request): View
{
return view('images');
}

/**
* Upload an image to local storage.
*
* @param Request $request
* @return RedirectResponse
*/
public function uploadPostImage(Request $request): RedirectResponse
{
$request->validate([
'upload_image' => 'required|image|mimes:jpeg,jpg,png|max:2048',
]);

// ~1 billion possible IDs, overlap potential should be small
$idMin = 1000000000;
$idMax = 9999999999;
$id = mt_rand($idMin, $idMax);

Image::make($request->file('upload_image'))->save(self::getImagePublicPath("$id.png"));

session()->flash('message', 'Image uploaded successfully.');
return redirect()->back();
}

/**
* Delete an image.
*
* @param Request $request
* @param int $id
* @return RedirectResponse
*/
public function deletePostImage(Request $request, int $id): RedirectResponse
{
// TODO: Store image IDs in a database
unlink(public_path("storage/images/posts/$id.png"));

session()->flash('message', 'Image deleted.');
return redirect()->back();
}

/**
* Generate reading time for an article.
*
Expand All @@ -310,26 +269,4 @@ public static function generateReadingTime(Post $post): int

return $readingTime;
}

/**
* Gets the image asset path for a post.
*
* @param string $imageName
* @return string
*/
public static function getImageAssetPath(string $imageName): string
{
return asset("storage/images/posts/$imageName");
}

/**
* Gets the public image path for a post.
*
* @param string $imageName
* @return string
*/
public static function getImagePublicPath(string $imageName): string
{
return public_path("storage/images/posts/$imageName");
}
}
28 changes: 23 additions & 5 deletions src/app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

namespace App\Http\Controllers;

use App\Models\Image;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
use Intervention\Image\ImageManagerStatic as Image;
use Intervention\Image\ImageManagerStatic;

class UserController extends Controller
{
Expand Down Expand Up @@ -68,6 +69,10 @@ public function updatePassword(Request $request): RedirectResponse
/**
* Update the user profile picture.
*
* 1. Store the image on disk
* 2. Make an entry of the image filename in the images table
* 3. Associate the new image to the user
*
* @param Request $request
* @return RedirectResponse
*/
Expand All @@ -76,11 +81,21 @@ public function updateProfilePic(Request $request): RedirectResponse
$request->validate([
'upload_profile_pic' => 'required|image|mimes:jpeg,jpg,png|max:2048',
]);
$id = $request->input('id');
$file = $request->file('upload_profile_pic');
$filename = ImageController::sanatizeImageFilename($file);

Image::make($request->file('upload_profile_pic'))
ImageManagerStatic::make($file)
->fit(150, 150)
->save(public_path("storage/images/avatars/$id.png"));
->save(ImageController::getImagePublicPath(ImageController::$avatarImagesSubdirectory, $filename));

$image = new Image();
$image->subdirectory = ImageController::$avatarImagesSubdirectory;
$image->filename = $filename;
$image->save();

$user = User::find(Auth::user()->id);
$user->image_id = $image->id;
$user->save();

session()->flash('message', 'Avatar updated successfully.');
return redirect()->back();
Expand All @@ -95,7 +110,10 @@ public function updateProfilePic(Request $request): RedirectResponse
*/
public function delete(Request $request, int $id): RedirectResponse
{
User::find($id)->delete();
$user = User::find($id);
$image = Image::find($user->image_id);
unlink(ImageController::getImagePublicPath($image->subdirectory, $image->filename));
$user->delete();

session()->flash('message', 'User deleted.');
return redirect()->back();
Expand Down
16 changes: 16 additions & 0 deletions src/app/Models/Image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
use HasFactory;

public function user()
{
return $this->belongsTo(User::class);
}
}
Loading

0 comments on commit 4a5fe4b

Please sign in to comment.