How to add DigitalOceans Spaces adapter to an Appwrite instance

Obisike Treasure - Sep 21 '23 - - Dev Community

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.

A screenshot showing the DigitalOcean platform

Click Store static objects, which takes you to the section for creating Spaces.

A screenshot showing the Spaces creation interface

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.

A screenshot showing the Create a Spaces Bucket

Your Spaces bucket will be created and assigned an Origin Endpoint.

A screenshot showing the created bucket

Next, you’ll need to enable anyone to access the content on your bucket. To do this, click on the Settings tab.

A screenshot showing the nuxt-gallery settings tab

Then on the File Listing section, click Edit.

A screenshot showing the file listing section

After that, select Enable File Listing.

A screenshot showing the Enable File Listing option

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.

A screenshot showing the bucket URL

This bucket URL contains the <bucket-name> and the <bucket-region> in the form:

https://<bucket-name>.<bucket-region>.digitaloceanspaces.com
Enter fullscreen mode Exit fullscreen mode

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.

A screenshot showing the API menu on the navigation bar

After that, click on Spaces Key.

A screenshot showing the Spaces Key tab

Then, in the Spaces Key section, click Generate New Token.

A screenshot of the Generate New Key button on the Spaces Key

Enter the name of the key, for this case, nuxt-media-gallery.

A screenshot showing the input to enter the name for your access keys

Then click the button indicated below to generate the key.

A screenshot showing the button to trigger generating the key

After that, you’ll have the access keys and secret.

A screenshot showing the Access Key and Access 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
Enter fullscreen mode Exit fullscreen mode

Create a directory named appwrite-spaces in the current working directory using the following:

mkdir appwrite-spaces 
cd appwrite-spaces
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Using the --remove-orphans flag will remove containers unrelated to the present services before initiating the new ones.

A screenshot showing the executing Docker compose command

After the services have been pulled by Docker from the Docker registry, the containers are then created.

A screenshot showing the competed execution of the command

After that, navigate to http://localhost to access your Appwrite instance.

A screenshot showing the landing page after visiting http://localhost

Next, click Sign up to create a new account.

A screenshot showing the sign up page

After signing up, enter your project’s name. For this case, use nuxt-media-gallery then click Create Project.

A screenshot showing the section to enter the project’s name

Creating the Appwrite storage for the project
To create the storage, Select Storage and click Create bucket.

A screenshot showing the Storage menu button and the Create Bucket button

After that, enter the name of the bucket. In this case, use nuxt-media-bucket, then click Create.

A screenshot showing the name input and the create button

Following that, you must grant CREATE and READ permissions to any user on this storage bucket. To do so, click the Settings tab.

A screenshot showing the settings tab

Then, navigate to the Permissions section, click Add a role to get started and select the Any option.

A screenshot showing the Add a role to get started button

After that, select the CREATE and READ permissions.

A screenshot showing the CREATE and READ checkbox

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>.

A screenshot showing the Settings menu

After that, find the Project ID and API Endpoint as indicated below.

A screenshot showing the Project ID and the API Endpoint

Next, to get the bucket ID, navigate to the Storage menu; there, you’ll see the bucket ID on the bucket as indicated below.

A screenshot showing the BucketID

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>
Enter fullscreen mode Exit fullscreen mode

In this case, replace the <project-name> with nuxt-media-gallery. Then run the command below:

cd <project-name>
Enter fullscreen mode Exit fullscreen mode

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:

To install, run the command:

yarn add appwrite swiper @nuxtjs/tailwindcss @vueuse/core
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

A screenshot showing the entry point of the application

After that, click Add Image, to add the images.

A screenshot showing the add images screen

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.

A screenshot showing the shared gallery

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.

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.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .