The rapid development of the Internet has created an unprecedented need for better storage. The current state of web development makes it crucial for applications to utilize services such as user authentication, file storage, and data delivery. Fortunately, Appwrite offers a solution that includes these services, eliminating the need for complex server setup and management.
What is Appwrite?
Appwrite is an all-in-one solution offering backend services such as authentication, databases, and file storage. It's user-friendly and can be self-hosted or used as a cloud service.
To overcome storage limitations encountered when using a self-hosted Appwrite instance, Appwrite allows you to extend its storage service using several external storage services, such as DigitalOcean Spaces.
What is DigitalOcean Spaces?
DigitalOcean Spaces is an object storage service offered by DigitalOcean, a popular cloud infrastructure provider. Spaces allows you to store and serve large amounts of data, such as images, videos, and other files, in a scalable and cost-effective manner.
This resilient, secure, and cost-effective file storage solution can seamlessly connect with Appwrite via an adapter, enhancing the storage service offered by the Appwrite instance.
Benefits of extending Appwrite storage service with DigitalOcean Spaces
Adding the DigitalOcean's Spaces adapter to an Appwrite instance essentially extends Appwrite's storage service. This extension offers the following advantages.
- Scalability and performance: As your application gains traction, the need for storage increases. DigitalOcean’s Spaces scales seamlessly, accommodating large data volumes. Merging it with Appwrite ensures smooth performance and efficient data handling.
- Cost efficiency: Unlike conventional server-based storage, Spaces lets you pay solely for used storage, a budget-friendly choice to expand your Appwrite app minus unnecessary costs.
- Backup and recovery made simple: With Spaces, crafting Appwrite backups is straightforward. Regular backups ensure that you're well-prepared for unforeseen data loss incidents.
Next, let's dive into extending your self-hosted Appwrite service with DigitalOcean’s Spaces by building a media-gallery application and adding the storage service.
Tutorial: Building a Nuxt.js media-gallery application
Here we’ll build a media gallery application using Nuxt.js, self-hosting Appwrite, extending Appwrite storage with DigitalOcean Spaces, and linking the storage to the application. The project repository can be found here. The application's functionalities will include:
- Creating image galleries.
- Storing and displaying created galleries.
- Generating shareable links for the galleries.
Prerequisites
The following requirements apply:
- Basic knowledge of JavaScript, Vue.js, and Nuxt.js.
- Node.js and Yarn installed on your computer.
- Docker installed for local hosting of Appwrite.
- A DigitalOcean account to create and access Spaces. ## Setting up DigitalOcean Spaces
To set up your DigitalOcean Spaces, create a new Spaces bucket on DigitalOcean and extract the Spaces Bucket access credentials.
Creating a new Spaces Bucket
To get started, log into your DigitalOcean account.
Click Store static objects, which takes you to the section for creating Spaces.
Afterward, keep the default values for Choose a datacenter region and the content delivery network (CDN). Enter your preferred bucket name in the Choose a unique Spaces Bucket name section. For this tutorial, you’ll be using nuxt-gallery.
Usually, a Spaces Bucket is linked to a project. For this guide, you’ll use the default project created by Digital Ocean when you sign up successfully.
Next, click Create a Spaces Bucket.
Your Spaces bucket will be created and assigned an Origin Endpoint.
Next, you’ll need to enable anyone to access the content on your bucket. To do this, click on the Settings tab.
Then on the File Listing section, click Edit.
After that, select Enable File Listing.
Getting the Spaces Access Keys
Next, you’ll need to retrieve the following keys:
<bucket-name>
<access-key>
<access-secret>
<bucket-region>
The bucket URL can be found in the Origin Endpoint indicated below.
This bucket URL contains the <bucket-name>
and the <bucket-region>
in the form:
https://<bucket-name>.<bucket-region>.digitaloceanspaces.com
You can then extract these keys from the URL on your dashboard.
Next, the <access-key>
can be obtained by navigating to API menu. Click on API.
After that, click on Spaces Key.
Then, in the Spaces Key section, click Generate New Token.
Enter the name of the key, for this case, nuxt-media-gallery.
Then click the button indicated below to generate the key.
After that, you’ll have the access keys and secret.
The Access Secret is located in the Secret row, and the Access Key is located in the nuxt-media-gallery row. Make a copy and save your Secret key because it will be lost if you leave the page.
Setting up Appwrite
To proceed, you‘ll need to locally host your Appwrite server. Let’s use Docker to accomplish this. Check out the installation guide to learn more about self-hosting Appwrite.
Locally hosting Appwrite with Docker
Ensure Docker is running by using the command below on your terminal to verify.
docker info
Create a directory named appwrite-spaces in the current working directory using the following:
mkdir appwrite-spaces
cd appwrite-spaces
Then, download the Appwrite base docker-compose.yml and .env files into the created folder.
Open the .env file and update the keys' values as follows:
_APP_STORAGE_DEVICE=DOSpaces
_APP_STORAGE_DO_SPACES_BUCKET=<bucket-name>
_APP_STORAGE_DO_SPACES_REGION=<bucket-region>
_APP_STORAGE_DO_SPACES_SECRET=<access-secret>
_APP_STORAGE_DO_SPACES_ACCESS_KEY=<access-key>
Setting _APP_STORAGE_DEVICE
from Local
to DOSpaces
indicates to Appwrite to use DigitalOcean Spaces instead of a locally implemented storage system.
Make sure you replace <bucket-name>
, <bucket-region>
, <access-secret>
, and <access-key>
(which are the keys obtained from the API section of the DigitalOcean dashboard).
Once that is done, execute this command within the appwrite-spaces directory:
docker compose up --remove-orphans -d
Using the --remove-orphans
flag will remove containers unrelated to the present services before initiating the new ones.
After the services have been pulled by Docker from the Docker registry, the containers are then created.
After that, navigate to http://localhost to access your Appwrite instance.
Next, click Sign up to create a new account.
After signing up, enter your project’s name. For this case, use nuxt-media-gallery then click Create Project.
Creating the Appwrite storage for the project
To create the storage, Select Storage and click Create bucket.
After that, enter the name of the bucket. In this case, use nuxt-media-bucket, then click Create.
Following that, you must grant CREATE and READ permissions to any user on this storage bucket. To do so, click the Settings tab.
Then, navigate to the Permissions section, click Add a role to get started and select the Any option.
After that, select the CREATE and READ permissions.
Extracting your Appwrite credentials
To retrieve your credentials, you need to get the values for the following:
<appwrite-bucket-id>
<appwrite-project-id>
<appwrite-api-endpoint>
Navigate to Settings by clicking the Settings menu to obtain the <appwrite-project-id>
.
After that, find the Project ID and API Endpoint as indicated below.
Next, to get the bucket ID, navigate to the Storage menu; there, you’ll see the bucket ID on the bucket as indicated below.
Setting up the Nuxt.js project
To set up the Nuxt.js project, open your terminal, and run the command below:
npx nuxi@latest init <project-name>
In this case, replace the <project-name>
with nuxt-media-gallery
. Then run the command below:
cd <project-name>
This changes the current working directory of the terminal to the project’s directory. Next, you’ll need to install all the dependencies.
The dependencies for this project include:
- Appwrite SDK
- Swiperjs (this will be used to perform the slide show)
- TailwindCSS
To install, run the command:
yarn add appwrite swiper @nuxtjs/tailwindcss @vueuse/core
Follow the steps here to set up TailwindCSS for the project.
After that, create an assets folder in the root directory and add the folders and files according to this link.
Adding the environmental variable
To add these variables, create a .env file in the root directory of the project and add the following to the file:
NUXT_APPWRITE_API_ENDPOINT=<appwrite-api-endpoint>
NUXT_APPWRITE_BUCKET_ID=<appwrite-bucket-id>
NUXT_APPWRITE_PROJECT_ID=<appwrite-project-id>
Replace the <appwrite-api-endpoint>
, <appwrite-bucket-id>
, and <appwrite-project-id>
values with the values you obtained from the Appwrite dashboard.
Creating the UI components and screens
To do so, first add the pages folder to the project's root directory. This is required for creating the application's routes/screens.
Next, navigate to the app.vue file and change the file's content to look like this:
<template>
<div class="h-screen">
<nuxt-page />
</div>
</template>
<script lang="ts">
import "swiper/css";
</script>
Landing screen
Following that, create a screen to collect the user's name so that the media files can be saved and identified.
In the pages folder, create a file, index.vue, and add the following:
<template>
<div class="x-container">
<div class="x-card">
<h1>Welcome to MShare</h1>
<p>Please provide your name so we can setup your unique profile</p>
<div class="mt-2">
<input
type="text"
class="w-full"
v-model="store.name"
placeholder="Name: e.g. Obisike Treasure"
/>
</div>
<div class="mt-2">
<input
type="button"
@click="navigateTo('/gallery')"
:disabled="!!!store.name"
class="w-full bg-orange-400 disabled:bg-orange-200 disabled:text-gray-600"
value="Create"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const store = useAppSession();
</script>
The vue template above consists of a card-like UI element with the heading "Welcome to MShare" and a paragraph prompting users to provide their name to create their gallery. Two input elements are present: one for the user's name and another for a "Create" button. The button is disabled until the user enters their name and then changes color. When users enter their name and click the button, the "create" method is triggered, allowing them to continue with the gallery creation.
Gallery screen
Following that, you need to add the route/screen for the users to upload their gallery. To do this, create a file gallery.vue in the pages directory, then add the following:
<template>
<div class="h-screen p-[3rem]">
<div class="x-card !max-w-full" v-if="store.previewKey">
<h3> Sharable Link:
<span class="bg-slate-200 inline-block px-2"> {{ location?.origin }}/share/{{ store.previewKey }} </span>
</h3>
</div>
<div class="x-card !max-w-full">
<h1> Select your media file:</h1>
</div>
<div class="x-card !max-w-full">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-[2rem]">
<transition-group appear name="fade">
<div class="w-full aspect-square x-border" v-for="(img, idx) in images" :key="img.idx">
<div class="w-full h-full relative">
<img :src="img.url" class="w-full h-full object-cover" />
<div class="absolute inset-0 backdrop:bg-slate-500">
<button class="bg-white rounded-md m-3 px-3" @click="removeImage(idx)">Remove</button>
</div>
</div>
</div>
</transition-group>
<div class="w-full aspect-square">
<label class="x-input__file w-full h-full">
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M450-450H200v-60h250v-250h60v250h250v60H510v250h-60v-250Z"/></svg>
<p> Add Image</p>
<input
type="file"
ref="imageField"
@change="setImages"
name="images"
multiple
class="hidden"
accept="image/*"
/>
</label>
</div>
</div>
</div>
<div class="x-card !max-w-full">
<button
class="bg-orange-300 disabled:bg-orange-100 w-full py-[1rem]"
@click="submit"
:disabled="!images?.length"
>{{ loading ? 'Uploading...': 'Upload' }}</button>
</div>
</div>
</template>
The template above consists of a container with multiple cards and grids. The first card displays a sharable link if the store.reviewKey
data exists. The second and third cards show static content. The fourth card contains a grid with images that can be added or removed using a Remove button. Users can upload multiple images using a hidden file input, and an Upload button is disabled until images are selected.
<script setup lang="ts">
import { ID } from "appwrite";
import { IFileImage } from "utils";
const config = useRuntimeConfig();
const store = useAppSession();
const loading = ref<boolean>()
const location = ref<Location>();
const images = ref<IFileImage[]>([]);
onMounted(() => {
location.value = window.location
})
function removeImage(idx: number) {
images.value.splice(idx, 1);
}
async function setImages(e: any) {
const files = (e.target as HTMLInputElement).files || [];
const fileImages = await Promise.all(
Array.from(files).map(async (file, idx) => ({
url: await convertToURL(file),
file,
idx
})),
);
images.value.push(...fileImages);
}
async function submit() {
try {
loading.value = true
await Promise.all(images.value.map((img, idx) => {
return pushToAppwrite(img.file, idx)
}))
store.value.previewKey = generateKey(images.value.length - 1);
alert("Successfully published!")
} finally {
loading.value = false
}
}
async function pushToAppwrite(file: File, idx: number) {
const storage = appwriteStorage();
await storage.createFile(
config.public.appwriteBucketId,
ID.custom(generateKey(idx)),
file
);
}
</script>
In the provided script, the pushToAppwrite()
function connects to Appwrite using credentials and uploads a given file object. A unique key is generated using generateKey()
for future file retrieval.
The setImages()
function extracts images from the file input and creates a base64 URL with convertToURL()
. This URL is then used to render the file blob as an image URL in a grid block.
The submit()
function uploads images using setImages()
to the images
state. It also generates a previewKey
for creating a shareable URL. To retrieve files with the shareable ID, the file ID has to be in this format:
<name>-<timestamp>-<length-of-array-of-images>
Share screen
Next, create the screen for the shareable link. Begin by creating a folder, share, within the pages directory. Inside the share folder, create a file called [pid].vue.
As per Nuxt guidelines, the route path is /share/[pid].
Then, append the specified content to the [pid].vue file.
<template>
<div class="py-[10%] px-[15%] w-full h-screen">
{{ route.params }}
<div class="flex flex-col w-full h-full">
<div class="x-card !max-w-full">
<h3> Obisike's Slideshows </h3>
</div>
<div class="pb-20">
<swiper
:effect="'cards'"
:grabCursor="true"
:autoplay="{
delay: 2500,
disableOnInteraction: true,
}"
:modules="modules"
>
<template v-for="(img, idx) in images" :key="idx">
<swiper-slide class="h-[20rem]">
<div class="w-full">
<img :src="img" class="w-full object-cover" loading="lazy" />
</div>
</swiper-slide>
</template>
</swiper>
</div>
</div>
</div>
</template>
<style scoped>
.swiper-cards{
overflow:visible;
height: 50%;
}
.swiper-cards .swiper-slide {
transform-origin:center bottom;
-webkit-backface-visibility:hidden;
backface-visibility:hidden;
overflow:hidden;
}
</style>
The template creates a slideshow using the Swiper library. It arranges images in a card-style layout, with a title given by the name of the gallery’s creator together with SlideShow, above the slideshow. The images transition using a cards effect and autoplay every 2.5 seconds. The template ensures responsiveness and lazy loading for images.
<script setup lang="ts">
import { Swiper, SwiperSlide } from "swiper/vue";
import { EffectCards, Autoplay } from "swiper/modules";
const modules = [Autoplay, EffectCards];
const config = useRuntimeConfig();
const route = useRoute();
const images = ref<string[]>([]);
onMounted(() => {
getImages()
})
async function getImages() {
const storage = appwriteStorage();
const keys = getFileIdsFromPreviewKey(route.params.pid as string);
if (!keys) return;
keys.forEach(k => {
const url = storage.getFileView(config.public.appwriteBucketId, k)
images.value.push(url.toString())
})
}
</script>
To break it down, in the script above, when the component loads, a function is triggered to fetch images, and then the getImage()
function gets the image from Appwrite using the route parameter pid
.
Additional modules, Autoplay and EffectCards, are applied to the Swiper component to enhance the slideshow with a card-stacking effect.
Next, you’ll need to add the utility functions.
Adding the utilities functions
To add utility functions, create a folder utils and a file index.ts within the utils folder. Then copy the contents of this file and paste them into the index.ts file.
The functions in this file is automatically imported due to Nuxt.js.
One of the notable functions you copied is the appwriteStorage()
function.
export function appwriteStorage() {
const config = useRuntimeConfig();
const client = new Client();
const storage = new Storage(client);
client
.setEndpoint(config.public.appwriteAPIEndpoint)
.setProject(config.public.appwriteProjectId);
return storage
}
This function obtains the environment variable via useRuntimeConfig()
, establishes a connection to Appwrite Storage service, and returns the storage object, which contains methods for retrieving and uploading files to appwrite.
Application demo
To showcase your application, start by accessing your terminal within the project directory, then execute the following command:
yarn dev
Once the command is successfully executed, the Nuxt.js development server will start on the port 3000.
Visit http://localhost:3000 on the browser to preview the screens.
Next, enter your desired name then click Create.
After that, click Add Image, to add the images.
Then click Upload to upload the selected images.
Once that is done, a shareable link will be generated. Copy and open this link in a different browser window.
Here is a video showcasing the application:
https://www.loom.com/share/0aac3ddd749748ad915b0f438837c6f6?sid=1daab281-7f22-40d4-ba9d-04cc45251084
To prove that the files are uploaded to Digital Oceans directly, here is a screenshot showing the file on Digital Oceans’ spaces.
Conclusion
In this tutorial, we've learned how to extend Appwrite's storage capabilities using DigitalOcean Spaces. By combining the strengths of these two services, developers can create robust applications with secure and scalable storage solutions.