Many social media based applications allow users to upload photos and these photos are usually displayed in a timeline for their followers and others to see. In the past, you would have had to refresh your feed manually to see new photos uploaded to the timeline. However, with modern web technologies, you can see the updates in realtime without having to refresh the page manually.
In this article, we will consider how you can build a realtime photo feed using Pusher Channels, GO and a little Vue.js. Pusher Channels helps you “easily build scalable in-app notifications, chat, realtime graphs, geotracking and more in your web & mobile apps with our hosted pub/sub messaging API.”
This is a preview of what we will be building:
Before we start building our application, make sure you have:
Let’s get started.
The first step will be to get a Pusher Channels application. We will need the application credentials for our realtime features to work.
Go to the Pusher website and create an account. After creating an account, you should create a new application. Follow the application creation wizard and then you should be given your application credentials, we will use this later in the article.
Now that we have our application, let’s move on to the next step
The next thing we want to do is create the Go application. In your terminal, cd
to your $GOPATH
and create a new directory there.
1$ cd $GOPATH/src 2 $ mkdir gofoto 3 $ cd gofoto
💡 It is recommended that you place the source code for your project in the
src
subdirectory (e.g.,$GOPATH/src/your_project
or$GOPATH/src/github.com/your_github_username/your_project
.
Next, we will create some directories to organize our application a little:
1$ mkdir database 2 $ mkdir public 3 $ mkdir public/uploads
This will create a database
and public
directory, and also an uploads
directory inside the public directory. We will store our database file inside the database
directory, we will keep our public files: HTML and images, inside the public
and uploads
directory. Create a new index.html
file in the public
directory that was created.
Now let’s create our first (and only) Go file for this article. We will try to keep everything simple by placing all our source code in a single file. Create a main.go
file in the project root.
In the file paste the following:
1package main 2 3 import ( 4 "database/sql" 5 "io" 6 "net/http" 7 "os" 8 9 "github.com/labstack/echo" 10 "github.com/labstack/echo/middleware" 11 _ "github.com/mattn/go-sqlite3" 12 pusher "github.com/pusher/pusher-http-go" 13 )
Above we have imported some packages we will be needing to work on our photo feed. We need the database/sql
to run SQL queries, the io
and os
package for our file uploading process, and the net/http
for our HTTP status codes.
We have some other external packages we imported. The labstack/echo
package is the Echo framework that we will be using. We also have the mattn/go-sqlite3
package which is for SQLite. Finally, we imported the pusher/pusher-http-go
package which we will use to trigger events to Pusher Channels.
Before we continue, let’s pull in these packages using our terminal. Run the following commands below to pull the packages in:
1$ go get github.com/labstack/echo 2 $ go get github.com/labstack/echo/middleware 3 $ go get github.com/mattn/go-sqlite3 4 $ go get github.com/pusher/pusher-http-go
Note that the commands above will not return any confirmation output when it finishes installing the packages. If you want to confirm the packages were indeed installed you can just check the
$GOPATH/src/github.com
directory.
Now that we have pulled in our packages, let’s create the main
function. This is the function that will be the entry point of our application. In this function, we will set up our applications database, middleware, and routes.
Open the main,go
file and paste the following code:
1func main() { 2 db := initialiseDatabase("database/database.sqlite") 3 migrateDatabase(db) 4 5 e := echo.New() 6 7 e.Use(middleware.Logger()) 8 e.Use(middleware.Recover()) 9 10 e.File("/", "public/index.html") 11 e.GET("/photos", getPhotos(db)) 12 e.POST("/photos", uploadPhoto(db)) 13 e.Static("/uploads", "public/uploads") 14 15 e.Logger.Fatal(e.Start(":9000")) 16 }
In the code above, we instantiated our database using the file path to the database file. This will create the SQLite file if it did not already exist. We then run the migrateDatabase
function which migrates the database.
Next, we instantiate Echo and then register some middlewares. The logger middleware is helpful for logging information about the HTTP request while the recover middleware “recovers from panics anywhere in the chain, prints stack trace and handles the control to the centralized HTTPErrorHandler.”
We then set up some routes to handle our requests. The first handler is the File
handler. We use this to serve the index.html
file. This will be the entry point to the application from the frontend. We also have the /photos
route which accepts a POST
and GET
request. We need these routes to act like API endpoints that are used for uploading and displaying the photos. The final handler is Static
. We use this to return static files that are stored in the /uploads
directory.
We finally use e.Start
to start our Go web server running on port 9000. The port is not set in stone and you can choose any available and unused port you feel like.
At this point, we have not created most of the functions we referenced in the main
function so let’s do so now.
In the main
function we referenced an initialiseDatabase
and migrateDatabase
function. Let’s create them now. In the main.go
file, paste the following functions above the main
function:
1func initialiseDatabase(filepath string) *sql.DB { 2 db, err := sql.Open("sqlite3", filepath) 3 if err != nil || db == nil { 4 panic("Error connecting to database") 5 } 6 7 return db 8 } 9 10 func migrateDatabase(db *sql.DB) { 11 sql := ` 12 CREATE TABLE IF NOT EXISTS photos( 13 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 14 src VARCHAR NOT NULL 15 ); 16 ` 17 18 _, err := db.Exec(sql) 19 if err != nil { 20 panic(err) 21 } 22 }
In the initialiseDatabase
function, we create an instance of the SQLite database using the database file and return that instance. In the migrateDatabase
function, we use the instance of the database returned in the previous function to execute the migration SQL.
Let’s create the data structure for our photo and photo collection.
The next thing we will do is create the data structure for our object types. We will create a Photo
structure and a PhotoCollection
structure. The Photo
struct will define how a typical photo will be represented while the PhotoCollection
will define how a collection of photos will be represented.
Open the main.go
file and paste the following code above the initialiseDatabase
function:
1type Photo struct { 2 ID int64 `json:"id"` 3 Src string `json:"src"` 4 } 5 6 type PhotoCollection struct { 7 Photos []Photo `json:"items"` 8 }
Next let’s create the functions for our routes. Open the main.go
file and paste the following file inside it:
1func getPhotos(db *sql.DB) echo.HandlerFunc { 2 return func(c echo.Context) error { 3 rows, err := db.Query("SELECT * FROM photos") 4 if err != nil { 5 panic(err) 6 } 7 8 defer rows.Close() 9 10 result := PhotoCollection{} 11 12 for rows.Next() { 13 photo := Photo{} 14 15 err2 := rows.Scan(&photo.ID, &photo.Src) 16 if err2 != nil { 17 panic(err2) 18 } 19 20 result.Photos = append(result.Photos, photo) 21 } 22 23 return c.JSON(http.StatusOK, result) 24 } 25 } 26 27 func uploadPhoto(db *sql.DB) echo.HandlerFunc { 28 return func(c echo.Context) error { 29 file, err := c.FormFile("file") 30 if err != nil { 31 return err 32 } 33 34 src, err := file.Open() 35 if err != nil { 36 return err 37 } 38 39 defer src.Close() 40 41 filePath := "./public/uploads/" + file.Filename 42 fileSrc := "http://127.0.0.1:9000/uploads/" + file.Filename 43 44 dst, err := os.Create(filePath) 45 if err != nil { 46 panic(err) 47 } 48 49 defer dst.Close() 50 51 if _, err = io.Copy(dst, src); err != nil { 52 panic(err) 53 } 54 55 stmt, err := db.Prepare("INSERT INTO photos (src) VALUES(?)") 56 if err != nil { 57 panic(err) 58 } 59 60 defer stmt.Close() 61 62 result, err := stmt.Exec(fileSrc) 63 if err != nil { 64 panic(err) 65 } 66 67 insertedId, err := result.LastInsertId() 68 if err != nil { 69 panic(err) 70 } 71 72 photo := Photo{ 73 Src: fileSrc, 74 ID: insertedId, 75 } 76 77 return c.JSON(http.StatusOK, photo) 78 } 79 }
In the getPhotos
method, we are simply running the query to fetch all the photos from the database and returning them as a JSON response to the client. In the uploadPhoto
method we first get the file to be uploaded then upload them to the server and then we run the query to insert a new record in the photos
table with the newly uploaded photo. We also return a JSON response from that function.
The next thing we want to do is trigger an event when a new photo is uploaded to the server. For this, we will be using the Pusher Go HTTP library.
In the main.go
file paste the following above the type definitions for the Photo
and PhotoCollection
:
1var client = pusher.Client{ 2 AppId: "PUSHER_APP_ID", 3 Key: "PUSHER_APP_KEY", 4 Secret: "PUSHER_APP_SECRET", 5 Cluster: "PUSHER_APP_CLUSTER", 6 Secure: true, 7 }
This will create a new Pusher client instance. We can then use this instance to trigger notifications to different channels we want. Remember to replace the PUSHER_APP_*
keys with the keys provided when you created your Pusher application earlier.
Next, go to the uploadPhoto
function in the main.go
file and right before the return
statement at the bottom of the function, paste the following code:
client.Trigger("photo-stream", "new-photo", photo)
This is the code that triggers a new event when a new photo is uploaded to our application.
That will be all for our Go application. At this point, you can build your application and compile it into a binary using the go build
command. However, for this tutorial we will just run the binary temporarily:
$ go run main.go
The next thing we want to do is build out our frontend. We will be using the Vue.js framework and the Axios library to send requests.
Open the index.html
file and in there paste the following code:
1<!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 6 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"> 7 <title>Photo Feed</title> 8 <style type="text/css"> 9 #photoFile { display: none; } 10 #app img { max-width: 100%; } 11 .image-row { margin: 20px 0; } 12 .image-row .thumbnail { padding: 2px; border: 1px solid #d9d9d9; } 13 </style> 14 </head> 15 <body> 16 <div id="app"> 17 18 <nav class="navbar navbar-expand-lg navbar-light bg-light"> 19 <a class="navbar-brand" href="#">GoFoto</a> 20 <div> 21 <ul class="navbar-nav mr-auto"> 22 <li class="nav-item active"> 23 <a class="nav-link" v-on:click="filePicker" href="#">Upload</a> 24 <input type="file" id="photoFile" ref="myFiles" @change="upload" name="file" /> 25 </li> 26 </ul> 27 </div> 28 </nav> 29 30 <div class="container"> 31 <div class="row justify-content-md-center" id="loading" v-if="loading"> 32 <div class="col-xs-12"> 33 Loading photos... 34 </div> 35 </div> 36 <div class="row justify-content-md-center image-row" v-for="photo in photos"> 37 <div class="col col-lg-4 col-md-6 col-xs-12"> 38 <img class="thumbnail" :src="photo.src" alt="" /> 39 </div> 40 </div> 41 </div> 42 43 </div> 44 <script src="//js.pusher.com/4.0/pusher.min.js"></script> 45 <script src="https://unpkg.com/axios/dist/axios.min.js"></script> 46 <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> 47 </body> 48 </html>
In the HTML file above we have defined the design for our photostream. We are using Bootstrap 4 and we included the CSS in the HTML above. We are also using the Axios library, Pusher library, and Vue framework. We included the links to the scripts at the bottom of the HTML document.
Next let’s add the Vue.js code. In the HTML file, add the following code right before the closing body
tag:
1<script type="text/javascript"> 2 new Vue({ 3 el: '#app', 4 data: { 5 photos: [], 6 loading: true, 7 }, 8 mounted() { 9 const pusher = new Pusher('PUSHER_APP_KEY', { 10 cluster: 'PUSHER_APP_CLUSTER', 11 encrypted: true 12 }); 13 14 let channel = pusher.subscribe('photo-stream') 15 16 channel.bind('new-photo', data => this.photos.unshift(data)); 17 18 axios.get('/photos').then(res => { 19 this.loading = false 20 this.photos = res.data.items ? res.data.items : [] 21 }) 22 }, 23 methods: { 24 filePicker: function () { 25 let elem = document.getElementById('photoFile'); 26 27 if (elem && document.createEvent) { 28 let evt = document.createEvent("MouseEvents"); 29 evt.initEvent("click", true, false); 30 elem.dispatchEvent(evt); 31 } 32 }, 33 upload: function () { 34 let data = new FormData(); 35 data.append('file', this.$refs.myFiles.files[0]); 36 37 axios.post('/photos', data).then(res => console.log(res)) 38 } 39 } 40 }); 41 </script>
Above we created a Vue instance and stored the properties photos
and loading
. The photos
property stores the photo list and the loading
just holds a boolean that indicates if the photos are loading or not.
In the mounted
method we create an instance of our Pusher library. We then listen on the photo-stream
channel for the new-photo
event. When the event is triggered we append the new photo from the event to the photos
list. We also send a GET request to /photos
to fetch all the photos from the API. Replace the PUSHER_APP_*
keys with the one from your Pusher dashboard.
In the methods
property, we added a few methods. The filePicker
is triggered when the ‘Upload’ button is pressed on the UI. It triggers a file picker that allows the user to upload photos. The upload
method takes the uploaded file and sends a POST request with the file to the API for processing.
That’s all for the frontend, you can save the file and head over to your web browser. Visit http://127.0.0.1:9000 to see your application in action.
Here’s how it will look again:
In this article, we have been able to demonstrate how you can use Pusher Channels in your Go application to provide realtime features for your application. As seen from the code samples above, it is very easy to get started with Pusher Channels. Check the documentation to see other ways you can use Pusher Channels to provide realtime features to your users.
The source code for this application is available on GitHub.