Build a File Sharing Server in Go
Build an HTTP file server in Go to upload and download files via a browser.
Introduction
30 Second Summary
Sharing files between devices shouldn't require signing up for a cloud service or emailing yourself attachments. What if you could spin up your own file sharing server in minutes, right on your computer?
In this project, you will build a local file sharing server in Go that lets you upload files through a web browser and download them later.
What You'll Build
A file sharing server built entirely with Go's standard library that handles multipart uploads, stores files to disk, and serves them back with proper content-type headers.
By the end of this project, you'll have:
- A working HTTP server in Go that accepts file uploads with size validation and serves downloads through the browser.
- An HTML interface for uploading, browsing, and downloading files without any external dependencies.
- Built-in security protections including path traversal prevention and filename sanitization.
- Secret Mission: Add MIME type validation to reject files that are not images or PDFs.
Are there any prerequisites?
None. This project includes full setup instructions for Go and your code editor. No prior Go experience is required.
Set Up Go and Your Project
To build a file sharing server, you need a Go project with its own module. A module is how Go organizes code and tracks dependencies.
In this step, you'll create a project folder, initialize a Go module, and write a tiny test server to confirm everything connects properly.
In this step, get ready to:
- Create a project folder and initialize a Go module.
- Write a basic HTTP server in Go.
- Run the server and confirm it responds in the browser.
Create your project folder and initialize the Go module
Every Go project lives inside a module. A module gives your project a name and lets Go manage any packages you import.
- Press the Windows key to open your search bar.
- Type Terminal and press Enter to open Windows Terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir fileshare
cd fileshare
- Initialize a Go module by running:
go mod init fileshare
What does this command do?
go mod init fileshare creates a go.mod file in your folder. This file declares your module name (fileshare) and the Go version you're using.
Think of it as your project's ID card. Go reads this file to know what your project is called and which Go version it targets.
- Confirm the file was created by running:
ls
You should see go.mod listed in the output.
Don't see go.mod?
Make sure you ran go mod init fileshare from inside the fileshare folder. Run pwd to check your current directory.
help me troubleshoot why go mod init didn't create a go.mod file
Create main.go with a Hello HTTP server
Before building the full file sharing server, you'll write a minimal server that responds with a greeting. This proves that Go's net/http package is working and your project compiles cleanly.
- In Cursor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the fileshare folder, then click Select Folder.
- Create a new file called main.go by clicking the new file icon in the Explorer sidebar.
- Add the following code to main.go:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// Register a handler for the root URL path
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
// Print a message so you know the server is running
fmt.Println("Server started at http://localhost:8080")
// Start the server on port 8080
log.Fatal(http.ListenAndServe(":8080", nil))
}
What does this code do?
- http.HandleFunc("/", ...) tells Go to call your function whenever someone visits the root URL (/).
- fmt.Fprintln(w, "Hello, World!") writes a greeting back to the visitor's browser.
- http.ListenAndServe(":8080", nil) starts the server on port 8080 and keeps it running until you stop it.
- log.Fatal(...) wraps the server start so that if anything goes wrong, the error prints and the program exits.
- Save main.go by pressing Ctrl+S.
Seeing red underlines in Cursor?
If Cursor shows red squiggly lines under the import paths, save the file first. The Go language server needs a saved file to resolve imports. If the lines persist after saving, check for typos in the package names (fmt, log, net/http).
help me fix red underlines in my Go file in Cursor
✔️ Awesome, I've got everything!
Great. Double check you've saved main.go before moving on.
ⓧ I'd like to double check the full code
Here's what main.go should look like:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Run the server and verify it works
Time to see your server in action. You'll start it from the terminal and visit it in a browser.
- In your terminal, make sure you're inside the fileshare folder.
- Start the server by running:
go run main.go
You should see this message printed in your terminal:
Server started at http://localhost:8080
- Open your web browser and visit http://localhost:8080.
You should see Hello, World! displayed on the page.
What is localhost:8080?
localhost means "this computer." Port 8080 is like a numbered door on your computer where the server listens for visitors.
When you visit http://localhost:8080 in your browser, you're telling the browser to connect to your own machine on port 8080. Your Go server is listening there, so it responds.
- Stop the server by pressing Ctrl+C in the terminal.
Browser shows "can't reach this page"?
- Check your terminal. If you don't see the "Server started" message, the server didn't start. Look for a compile error and fix any typos in main.go.
- Make sure you typed http://localhost:8080 exactly. No extra slashes or port numbers.
- If you see "address already in use," another program is using port 8080. Close other servers or try restarting your terminal.
help me debug why my Go server isn't responding in the browser
Your Go environment is ready and your first server works. Next up, you'll build the upload handler that accepts files from the browser and saves them to disk.
Build the Upload Handler
Your Go environment is ready and your Hello World server proved that everything works. Now it is time to give the server a real job.
In this step, you will build an upload handler that receives files from a web browser, enforces a size limit, and saves them to disk. This covers the core file-upload flow that every file sharing service needs.
In this step, get ready to:
- Configure an uploads directory and a 10 MB file size limit.
- Implement an upload handler that parses multipart form data and saves files.
- Return a JSON response confirming the upload.
Create the uploads directory and configure size limits
Before the server can accept files, it needs a folder to store them and a rule about how large uploads can be. You will add two constants and update main() to create the folder on startup.
- In Cursor, open main.go and replace the entire file content with this code:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
const uploadDir = "uploads"
const maxUploadSize = 10 << 20 // 10 MB
func main() {
// Ensure uploads directory exists
if err := os.MkdirAll(uploadDir, 0755); err != nil {
log.Fatalf("Failed to create uploads directory: %v", err)
}
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
What does this code do?
uploadDir defines the folder name where uploaded files will live. maxUploadSize uses a bit-shift (10 << 20) to express 10 megabytes in bytes.
os.MkdirAll creates the uploads directory (and any parent folders) if it does not already exist. If it fails, log.Fatalf prints the error and stops the program immediately.
- Save main.go.
- Run the server to verify the directory gets created:
go run main.go
- You should see Server started at http://localhost:8080 in the terminal.
- Open a second terminal and confirm the folder exists by running:
ls uploads
- You should see an empty directory listing with no errors.
- Stop the server by pressing Ctrl+C in the first terminal.
Don't see the uploads folder?
Make sure you saved main.go before running go run main.go. Check that the const uploadDir line reads exactly "uploads" with quotes.
help me debug my uploads directory not being created
Implement the upload handler function
The upload handler is the function that receives an incoming file, checks its size, and writes it to the uploads folder. It uses Go's built-in multipart/form-data parsing to handle browser file uploads.
- In main.go, find the import block at the top of the file:
import (
"fmt"
"log"
"net/http"
"os"
)
- Replace the import block with this expanded version that includes packages for JSON encoding, file copying, and path handling:
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
- Now add the complete uploadHandler function below the closing brace of main(). Copy this entire block:
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
// Parse multipart form with 10 MB max memory
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
http.Error(w, "File too large (max 10 MB)", http.StatusBadRequest)
return
}
// Retrieve the file from the form
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
// Sanitize filename
filename := filepath.Base(header.Filename)
if filename == "." || filename == "/" {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
// Create destination file
dstPath := filepath.Join(uploadDir, filename)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy uploaded file to destination
written, err := io.Copy(dst, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
// Return JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"message": "File uploaded successfully",
"filename": filename,
"size": written,
})
}
- Finally, add this line at the very bottom of the file (after the closing brace of uploadHandler):
_ = strings.Contains
Why is this line here?
Go refuses to compile if an import is unused. The strings package is not used by the upload handler, but you will need it in a later step. This blank-identifier line satisfies the compiler until then.
What does the upload handler do?
The function works in three phases. First, it enforces security: http.MaxBytesReader caps incoming data at 10 MB so a huge upload cannot exhaust server memory. r.ParseMultipartForm then parses the browser's form data, and r.FormFile("file") extracts the uploaded file.
Second, it saves the file: filepath.Base strips any directory path from the filename to prevent directory traversal attacks. os.Create opens a new file in the uploads folder, and io.Copy streams the uploaded bytes into it.
Third, it responds: the handler sends back a JSON object containing the filename and byte count, with a 201 Created status code. This confirms to the client that the upload succeeded.
- Save main.go.
- Verify the code compiles by running:
go build
- If the command finishes with no output, your code compiled successfully.
Seeing a compile error?
If you see an "imported and not used" error, make sure the _ = strings.Contains line is at the bottom of the file, outside any function.
If you see a syntax error, check that every opening brace { has a matching closing brace }. Count the braces in your uploadHandler function carefully.
help me fix my Go compile error in main.go
Understand the JSON response pattern
The last section of your handler builds a structured response so that any client (browser, mobile app, or API tool) knows the upload worked.
Why return JSON instead of plain text?
JSON responses make your server interoperable. A browser can display the filename, a mobile app can parse the byte count, and an API testing tool can validate the status code. This pattern is what real production APIs use.
json.NewEncoder(w).Encode(...) writes the Go map directly to the HTTP response body as a JSON object in a single step.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.go and that go build produces no errors.
ⓧ I'd like to double check the full code
Here is the complete main.go as it should look at the end of this step:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
const uploadDir = "uploads"
const maxUploadSize = 10 << 20 // 10 MB
func main() {
// Ensure uploads directory exists
if err := os.MkdirAll(uploadDir, 0755); err != nil {
log.Fatalf("Failed to create uploads directory: %v", err)
}
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
// Parse multipart form with 10 MB max memory
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
http.Error(w, "File too large (max 10 MB)", http.StatusBadRequest)
return
}
// Retrieve the file from the form
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
// Sanitize filename
filename := filepath.Base(header.Filename)
if filename == "." || filename == "/" {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
// Create destination file
dstPath := filepath.Join(uploadDir, filename)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy uploaded file to destination
written, err := io.Copy(dst, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
// Return JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"message": "File uploaded successfully",
"filename": filename,
"size": written,
})
}
_ = strings.Contains
Your upload handler is ready to receive files. Next up, you will build the download handler so users can retrieve the files they have uploaded.
Build the Download Handler
Your upload handler is ready to receive files. Now you need the other half of the equation: letting people download those files.
In this step, you'll build a downloadHandler function that serves files back to the browser. It will extract the requested filename from the URL, verify the file exists, and serve it with the correct content type.
In this step, get ready to:
- Implement a download handler that extracts filenames from URL paths.
- Serve files with proper content-type headers using http.ServeFile.
- Add path traversal protection to block malicious file requests.
Add the download handler function
The download handler needs to do three things: figure out which file the user wants, confirm it exists, and send it back. Go's standard library has built-in helpers for all of these.
- In main.go, find the line _ = strings.Contains at the bottom of the file and delete it.
- Add the following downloadHandler function below your uploadHandler function:
func downloadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract filename from path
filename := strings.TrimPrefix(r.URL.Path, "/files/")
if filename == "" {
http.Error(w, "Filename required", http.StatusBadRequest)
return
}
// Sanitize filename - prevent path traversal
filename = filepath.Base(filename)
if strings.Contains(filename, "..") {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
// Build full path and verify file exists
filePath := filepath.Join(uploadDir, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
// Serve the file
http.ServeFile(w, r, filePath)
}
What does this code do?
- strings.TrimPrefix(r.URL.Path, "/files/") strips the route prefix from the URL, leaving just the filename. A request to /files/photo.jpg becomes photo.jpg.
- filepath.Base and the .. check prevent path traversal attacks (more on this below).
- os.Stat checks whether the file actually exists on disk before trying to serve it.
- http.ServeFile sends the file to the browser with the correct Content-Type header detected automatically.
What is path traversal?
Path traversal is a security attack where someone requests a filename like ../../etc/passwd to escape the uploads folder and access system files.
filepath.Base strips all directory components, turning ../../etc/passwd into just passwd. The strings.Contains(filename, "..") check adds an extra layer of defense by rejecting anything that still contains .. after sanitization.
How does http.ServeFile work?
http.ServeFile does a lot of heavy lifting behind the scenes. It detects the file's content type from its extension, sets the correct headers, and even supports range requests (which let browsers resume interrupted downloads).
You get all of this for free from Go's standard library. No external packages needed.
- Save main.go.
- Verify your code compiles by running:
go build
If the command completes with no output, your code compiled successfully. You'll see a fileshare.exe file appear in your project folder.
Seeing a compile error?
- Make sure you deleted the _ = strings.Contains line. Leaving it in won't cause a compile error, but it's dead code you no longer need.
- Check that the opening brace { of downloadHandler is on the same line as func. Go requires this.
- Confirm you have "strings" in your import block at the top of the file.
Still stuck? Help me fix my downloadHandler compile error
✔️ Awesome, I've got everything!
Great. Make sure you've saved main.go before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.go file as it should look at this point:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
const uploadDir = "uploads"
const maxUploadSize = 10 << 20 // 10 MB
func main() {
// Ensure uploads directory exists
if err := os.MkdirAll(uploadDir, 0755); err != nil {
log.Fatalf("Failed to create uploads directory: %v", err)
}
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
// Parse multipart form with 10 MB max memory
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
http.Error(w, "File too large (max 10 MB)", http.StatusBadRequest)
return
}
// Retrieve the file from the form
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
// Sanitize filename
filename := filepath.Base(header.Filename)
if filename == "." || filename == "/" {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
// Create destination file
dstPath := filepath.Join(uploadDir, filename)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy uploaded file to destination
written, err := io.Copy(dst, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
// Return JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"message": "File uploaded successfully",
"filename": filename,
"size": written,
})
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract filename from path
filename := strings.TrimPrefix(r.URL.Path, "/files/")
if filename == "" {
http.Error(w, "Filename required", http.StatusBadRequest)
return
}
// Sanitize filename - prevent path traversal
filename = filepath.Base(filename)
if strings.Contains(filename, "..") {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
// Build full path and verify file exists
filePath := filepath.Join(uploadDir, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
// Serve the file
http.ServeFile(w, r, filePath)
}
Your download handler is locked and loaded. Next up, you'll build the HTML interface that ties uploading and downloading together into a usable web page.
Add the HTML Interface and File Listing
Your upload and download handlers are ready to process files. Now your server needs a front door.
In this step, you will create an HTML page that lets users pick a file, upload it, and browse everything already stored on the server. You will also register all your handlers to their routes so the server knows which function handles which URL.
In this step, get ready to:
- Create a homeHandler function that serves an HTML upload form and lists uploaded files.
- Wire up all handlers in main() with route registrations.
- Run the server and test uploading and downloading files through your browser.
Create the home handler
Your server needs a handler for the root URL (/) that shows visitors a page with an upload form and a list of all files currently stored in the uploads directory.
- In main.go, add the following homeHandler function below the main() function (above uploadHandler):
func homeHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
files, err := os.ReadDir(uploadDir)
if err != nil {
http.Error(w, "Failed to list files", http.StatusInternalServerError)
return
}
var fileListHTML strings.Builder
for _, file := range files {
if !file.IsDir() {
name := file.Name()
fileListHTML.WriteString(fmt.Sprintf(`<li><a href="/files/%s">%s</a></li>`, name, name))
}
}
html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Share</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 0 20px; }
h1 { color: #333; }
form { margin: 20px 0; padding: 20px; border: 2px dashed #ccc; border-radius: 8px; }
input[type="file"] { margin: 10px 0; }
input[type="submit"] { background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
input[type="submit"]:hover { background: #45a049; }
ul { list-style: none; padding: 0; }
li { padding: 8px 0; border-bottom: 1px solid #eee; }
a { color: #1a73e8; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>File Share</h1>
<form enctype="multipart/form-data" action="/upload" method="POST">
<p>Choose a file to upload:</p>
<input type="file" name="file" required />
<br/>
<input type="submit" value="Upload" />
</form>
<h2>Uploaded Files</h2>
<ul>%s</ul>
<p><em>No files yet? Upload one above!</em></p>
</body>
</html>`, fileListHTML.String())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
What does this code do?
This handler does three things before sending the page to the browser:
- The r.URL.Path != "/" check ensures only the exact root path triggers this handler. Any other path gets a 404 response.
- os.ReadDir(uploadDir) scans the uploads folder and builds an HTML list of clickable download links for each file it finds.
- The fmt.Sprintf call injects the file list into a full HTML template that includes a styled upload form and a file listing section.
- You will not see a change yet. The next substep wires this handler to a route so the server knows to call it.
Wire up all handlers in main()
Go's http.HandleFunc registers a function to handle requests for a specific URL pattern. Your server needs three routes: one for the home page, one for uploads, and one for file downloads.
- In main.go, replace your entire main() function with this updated version:
func main() {
// Ensure uploads directory exists
if err := os.MkdirAll(uploadDir, 0755); err != nil {
log.Fatalf("Failed to create uploads directory: %v", err)
}
http.HandleFunc("/", homeHandler)
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/files/", downloadHandler)
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
What do these routes mean?
- "/" matches the root URL and calls homeHandler to show the HTML page.
- "/upload" matches the upload endpoint and calls uploadHandler to process file submissions.
- "/files/" matches any path starting with /files/ and calls downloadHandler to serve the requested file.
- Save main.go.
Seeing a compile error?
- Make sure homeHandler is defined as a function in your file. Go will not compile if a route references an undefined function.
- Check that homeHandler is placed between main() and uploadHandler in your file.
- help me fix a compile error in my Go file server
Run the server and test in your browser
Your code is complete. Time to start the server and verify that uploading and downloading both work through the browser interface.
- Run the server by entering this command in your terminal:
go run main.go
- You should see Server started at http://localhost:8080 printed in the terminal.
- Open your browser and go to http://localhost:8080.
You should see the File Share page with a dashed upload box and an empty file list.
- Click the file picker and select any file from your computer (under 10 MB).
- Click Upload.
- Refresh the page at http://localhost:8080. Your uploaded file should now appear in the list.
- Click the file link to download it and confirm the file opens correctly.
Why do I have to refresh?
The upload endpoint returns a JSON response, not a redirect. After uploading, your browser shows raw JSON. Refreshing the home page at / re-runs homeHandler, which reads the uploads directory and shows the updated file list.
Page not loading?
- Make sure no other program is using port 8080. Stop any previous go run processes by pressing Ctrl+C in the terminal first.
- Check that you saved main.go before running go run main.go. Unsaved changes will not compile.
- If you see a Windows firewall prompt, click Allow access to let the server listen on the network.
- help me troubleshoot my Go server not loading in the browser
✔️ Awesome, I've got everything!
Great. Double check that you have saved main.go and your server is still running.
ⓧ I'd like to double check the full code
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
const uploadDir = "uploads"
const maxUploadSize = 10 << 20 // 10 MB
func main() {
// Ensure uploads directory exists
if err := os.MkdirAll(uploadDir, 0755); err != nil {
log.Fatalf("Failed to create uploads directory: %v", err)
}
http.HandleFunc("/", homeHandler)
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/files/", downloadHandler)
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
files, err := os.ReadDir(uploadDir)
if err != nil {
http.Error(w, "Failed to list files", http.StatusInternalServerError)
return
}
var fileListHTML strings.Builder
for _, file := range files {
if !file.IsDir() {
name := file.Name()
fileListHTML.WriteString(fmt.Sprintf(`<li><a href="/files/%s">%s</a></li>`, name, name))
}
}
html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Share</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 0 20px; }
h1 { color: #333; }
form { margin: 20px 0; padding: 20px; border: 2px dashed #ccc; border-radius: 8px; }
input[type="file"] { margin: 10px 0; }
input[type="submit"] { background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
input[type="submit"]:hover { background: #45a049; }
ul { list-style: none; padding: 0; }
li { padding: 8px 0; border-bottom: 1px solid #eee; }
a { color: #1a73e8; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>File Share</h1>
<form enctype="multipart/form-data" action="/upload" method="POST">
<p>Choose a file to upload:</p>
<input type="file" name="file" required />
<br/>
<input type="submit" value="Upload" />
</form>
<h2>Uploaded Files</h2>
<ul>%s</ul>
<p><em>No files yet? Upload one above!</em></p>
</body>
</html>`, fileListHTML.String())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
http.Error(w, "File too large (max 10 MB)", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
filename := filepath.Base(header.Filename)
if filename == "." || filename == "/" {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
dstPath := filepath.Join(uploadDir, filename)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
written, err := io.Copy(dst, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"message": "File uploaded successfully",
"filename": filename,
"size": written,
})
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
filename := strings.TrimPrefix(r.URL.Path, "/files/")
if filename == "" {
http.Error(w, "Filename required", http.StatusBadRequest)
return
}
filename = filepath.Base(filename)
if strings.Contains(filename, "..") {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
filePath := filepath.Join(uploadDir, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
http.ServeFile(w, r, filePath)
}
Secret mission
Add MIME Type Validation
Right now, your file server accepts anything. Someone could upload an executable disguised as a .jpg. Can you add real content-type detection that only allows images and PDFs through?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The fileshare project folder on your Desktop (contains main.go, go.mod, and the uploads directory with any uploaded files).
- Go 1.26 installed via the Windows MSI installer.
Keep everything running
No action needed. Choose this if you plan to keep building on your file sharing server or experimenting with Go.
- Your project folder stays on your Desktop.
- Go remains installed and available for other projects.
- Any files in the uploads directory stay on disk.
- The server is not running unless you start it again with go run main.go.
Pause - I'll come back to this later
Stop the running server to free up the terminal. Your project files stay on disk so you can resume later.
- If the server is still running, press Ctrl+C in the terminal to stop it.
- Your fileshare folder and all uploaded files remain on your Desktop.
- To restart later, open a terminal, navigate to your project folder, and run go run main.go.
Delete - I don't want to use this again
Remove all project files and optionally uninstall Go from your system.
- If the server is still running, press Ctrl+C in the terminal to stop it.
Delete the project folder by running this command in your terminal:
cd ~/Desktop
rmdir /s /q fileshare
If you are on macOS or Linux, run this instead:
cd ~/Desktop
rm -rf fileshare
- To uninstall Go on Windows, open Settings, then Apps, then Apps & features.
- Find Go Programming Language in the list and click Uninstall.
- Verify Go is removed by opening a new terminal and running:
go version
You should see an error indicating the command is not recognized.
Nice Work!
Nice Work!
Nice work! You just built a fully functional file sharing server from scratch using only Go's standard library.
You've learned how to:
- Built an HTTP server using Go's net/http package that handles multipart file uploads with size validation.
- Served files for download with automatic content-type detection and path traversal protection.
- Created a web interface for uploading and browsing files without any external dependencies.
- Added MIME type validation using http.DetectContentType to filter uploads by actual file content.
Ready to quiz yourself?