Welcome to the final chapter of our core backend tutorial!
In Chapter 6: Frontend View Layer, we built a beautiful interface that displays lists of files. However, there is one big problem: Everything looks the same.
Whether it is a 4K movie, a PDF contract, or a vacation photo, the user just sees a generic icon. To see the content, they have to download the whole file.
In this chapter, we are building the Media Preview Pipeline. This is a specialized "Darkroom" inside our server that takes massive files, extracts a single frame or page, and creates a tiny image (thumbnail) that loads instantly.
Imagine you have a folder with 50 video files, each 1GB in size.
The pipeline is a series of steps:
preview package)
This is the manager. It decides which tool to use. If you ask for a preview of party.mp4, the Service knows to call the Video Processor. If you ask for photo.jpg, it calls the Image Resizer.
For videos, we use an external tool called FFmpeg. It is a command-line program that can "seek" to a specific second in a video and take a screenshot.
Generating a thumbnail from a video takes a lot of CPU power. We never want to do it twice. Once we generate an image, we save it in a hidden folder (e.g., .filebrowser/cache/thumbnails). Next time, we serve the file from the cache instantly.
Let's look at the scenario: A user scrolls down to see movie.mp4.
movie.mp4.cache/movie_mp4_small.jpg exists.Here is how the HTTP handler (from Chapter 3) talks to the Preview Service.
// backend/http/preview.go (Conceptual)
func getPreview(file iteminfo.ExtendedFileInfo) ([]byte, error) {
// 1. Get the global preview service
service := preview.GetService()
// 2. Ask for a preview (Size: "small", Seek: 10%)
// This function handles caching and generation automatically.
imgData, err := preview.GetPreviewForFile(
ctx,
file,
"small", // Preview size
"", // URL (unused for local files)
10 // Seek percentage (10% into the video)
)
return imgData, err
}
Explanation:
preview.GetService(): Grabs the running instance of our media lab.GetPreviewForFile: The main entry point. It hides all the complexity of FFmpeg and Caching from the rest of the app.How does the code decide what to do? It follows a strict decision tree.
In backend/preview/preview.go, the generateRawPreview function acts as the traffic cop.
// backend/preview/preview.go
func (s *Service) generateRawPreview(ctx context.Context, file iteminfo.ExtendedFileInfo, ...) ([]byte, error) {
// 1. Figure out what kind of file this is
previewType := determinePreviewType(file)
switch previewType {
case previewTypeImage:
// Use standard Go image library
return s.generateImagePreview(ctx, file, previewSize)
case previewTypeVideo:
// Use FFmpeg external command
return s.generateVideoPreviewBytes(ctx, file, seekPercentage)
case previewTypeDocument:
// Use PDF library
return s.generateDocumentPreview(ctx, file, hash)
default:
return nil, errors.New("unknown preview type")
}
}
Explanation:
determinePreviewType: Checks the file extension (e.g., .jpg, .mp4) or MIME type.
Generating a video thumbnail is the most complex part. We use the ffmpeg package to run a command.
// backend/ffmpeg/video.go
func (s *FFmpegService) GenerateVideoPreviewStreaming(ctx context.Context, path string, ...) error {
// 1. Construct the command
// "ffmpeg -ss [time] -i [video] -frames 1 -f image2 -"
cmd := exec.CommandContext(ctx, s.ffmpegPath,
"-ss", seekTimeStr, // Jump to specific second
"-i", videoPath, // Input file
"-vcodec", "mjpeg", // Output format
"-", // Write to Standard Output (RAM)
)
// 2. Run the command and capture the image data
cmd.Stdout = writer
return cmd.Run()
}
Explanation:
-ss (Seek): This is crucial. We jump quickly to the middle of the video. We don't read the whole file.-vcodec mjpeg: We tell FFmpeg to create a JPEG image.-: Instead of saving to a file on disk, we stream the bytes directly back to our Go program. This is faster and uses less disk I/O.What happens if 100 users try to generate previews for 100 different 4K movies at the exact same time? The server would crash. FFmpeg uses a lot of CPU.
To prevent this, the preview package uses Semaphores (channels). Think of this as a nightclub bouncer.
// backend/preview/preview.go
// Create a channel that can hold only X items (e.g., 4)
imageSem = make(chan struct{}, 4)
func (s *Service) acquireImageSem(ctx context.Context) error {
select {
case s.imageSem <- struct{}{}:
// 1. Success! We entered the "club". Proceed to processing.
return nil
case <-ctx.Done():
// 2. Timeout or Cancelled.
return ctx.Err()
}
}
Explanation:
Before starting FFmpeg, the code calls acquireImageSem.
This ensures that your Raspberry Pi or server doesn't freeze when you open a folder full of movies.
Finally, we need to ensure we don't repeat work. We use an MD5 hash to create a unique ID for every file state.
// backend/preview/preview.go
func GeneratePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, ...) {
// 1. Create a unique string based on Path + Size + ModTime
cacheString := fmt.Sprintf("%s:%d:%s", file.RealPath, file.Size, file.ModTime)
// 2. Hash it to get a filename (e.g., "a1b2c3d4...")
hasher := md5.New()
hasher.Write([]byte(cacheString))
cacheHash := hex.EncodeToString(hasher.Sum(nil))
// 3. Check if this hash exists on disk
if data, found, _ := service.fileCache.Load(ctx, cacheHash); found {
return data, nil // Return cached image immediately
}
// 4. If not found, start generation...
}
Explanation:
We include ModTime (Modification Time) in the hash. If you edit a photo or replace a video, the ModTime changes, the hash changes, and the system automatically generates a new preview. You never see an old thumbnail for a new file.
In this final chapter, we turned our file manager into a rich media gallery.
Congratulations! You have explored the entire architecture of Filebrowser.
You now have a deep understanding of how a modern, full-stack Go application is structured. Happy coding!
Generated by Code IQ