Celo-based Decentralized File Sharing Platform

Introduction

In this second part of our tutorial series, we will significantly enhance our decentralized file storage platform, which we built on the Celo blockchain in the first part. By adding a comprehensive file-sharing feature, we’ll transform our platform into a robust, user-friendly decentralized file sharing platform. We will not only be extending the smart contract but also greatly improving the user interface, adding various new features to create an engaging, easy-to-use application. Let’s get started!

Prerequisites

Before we dive into this tutorial, ensure you have completed the first part of the series - Implementing Celo-based Decentralized Storage. We will be extending the code written in that tutorial. You should also be comfortable with blockchain concepts, Solidity for contract development, JavaScript for scripting, and React for frontend development.

Requirements

Ensure these software are installed on your system:

  1. Node.js: To run our scripts.
  2. Metamask: A digital wallet used to interact with the Celo blockchain.
  3. Truffle: A development environment, testing framework, and asset pipeline for Ethereum Virtual Machine (EVM) compatible blockchains.

Building the File Sharing Platform

Updating the Smart Contract

Let’s start with the contract. We’ll be adding new methods to enable file sharing and retrieval. The new methods are shareFile, which allows a user to share a file with others, and getSharedFiles, which retrieves a list of files shared with a user.

// ... existing code ...

mapping (address => string[]) sharedFileHashes;

function shareFile(string memory _fileHash, address _recipient) public {
    require(bytes(fileHashes[msg.sender]).length > 0, "File not found");
    sharedFileHashes[_recipient].push(_fileHash);
}

function getSharedFiles() public view returns (string[] memory) {
    return sharedFileHashes[msg.sender];
}

Considering the functionalities of our existing contract, and the desired enhancement to include file sharing functionality, the updated FileStorage.sol will look as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FileStorage {
    struct File {
        string ipfsHash;
        address owner;
        bool isPublic;
        mapping(address => bool) authorizedUsers;
    }

    mapping(bytes32 => File) private files;
    mapping(address => string[]) private sharedFileHashes;

    event FileAdded(bytes32 indexed fileId, string ipfsHash, address indexed owner, bool isPublic);
    event FileAccessGranted(bytes32 indexed fileId, address indexed user);
    event FileAccessRevoked(bytes32 indexed fileId, address indexed user);
    event FileShared(bytes32 indexed fileId, address indexed recipient);

    function addFile(string memory ipfsHash, bool isPublic) public {
        bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
        require(files[fileId].owner == address(0), "File already exists");

        files[fileId].ipfsHash = ipfsHash;
        files[fileId].owner = msg.sender;
        files[fileId].isPublic = isPublic;

        emit FileAdded(fileId, ipfsHash, msg.sender, isPublic);
    }

    function getFile(string memory ipfsHash) public view returns (string memory, bool) {
        bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
        File storage file = files[fileId];
        require(file.owner != address(0), "File does not exist");
        require(file.isPublic || file.owner == msg.sender || file.authorizedUsers[msg.sender], "Access denied");

        return (file.ipfsHash, file.isPublic);
    }

    function grantAccess(string memory ipfsHash, address user) public {
        bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
        File storage file = files[fileId];
        require(file.owner == msg.sender, "Only the file owner can grant access");

        file.authorizedUsers[user] = true;
        emit FileAccessGranted(fileId, user);
    }

    function revokeAccess(string memory ipfsHash, address user) public {
        bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
        File storage file = files[fileId];
        require(file.owner == msg.sender, "Only the file owner can revoke access");

        file.authorizedUsers[user] = false;
        emit FileAccessRevoked(fileId, user);
    }

    function shareFile(string memory ipfsHash, address recipient) public {
        bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
        File storage file = files[fileId];
        require(file.owner == msg.sender, "Only the file owner can share the file");

        file.authorizedUsers[recipient] = true;
        sharedFileHashes[recipient].push(ipfsHash);
        emit FileShared(fileId, recipient);
    }

    function getSharedFiles() public view returns (string[] memory) {
        return sharedFileHashes[msg.sender];
    }
}

This updated contract now includes a mapping sharedFileHashes to keep track of the files that each user has received. The shareFile function ensures only the file owner can share their files. After successfully sharing, the function stores the IPFS hash of the shared file in the recipient’s array of shared files.

Also, the getSharedFiles function allows a user to retrieve all the file hashes that have been shared with them. Please note that it returns only the IPFS hashes of the files. Users will still need to use the getFile function to access the shared files.

Compiling and Deploying the Smart Contract

We would then update and redeploy our update contract for the changes to take effect.

  • Run the following command to compile the smart contract:

truffle compile

  • Run the following command to deploy the FileStorage smart contract to the Celo network:

truffle migrate --network alfajores

You should note the contract address for later use in our code.

Enhancing the Front End

Next, we will update our frontend application to utilize these new contract methods. Let’s break down the features we want to implement:

Sharing Files

First, we want users to be able to share their files. For this, we will create a new ShareFile component in React.
1. ShareFile Component.
Create a ShareFile.js file and add this code:

import { Card, TextField, Button, Typography, Grid } from "@material-ui/core";

export default function ShareFile() {
  const [fileHash, setFileHash] = useState('');
  const [recipient, setRecipient] = useState('');

  const shareFile = async () => {
    const result = await contract.methods.shareFile(fileHash, recipient).send({ from: account });
    console.log(result);
  }

  return (
    <Card style={{ margin: "20px", padding: "20px" }}>
      <Typography variant="h5">Share File</Typography>
      <Grid container spacing={3} alignItems="center">
        <Grid item xs={12}>
          <TextField
            fullWidth
            label="File Hash"
            variant="outlined"
            value={fileHash}
            onChange={e => setFileHash(e.target.value)}
          />
        </Grid>
        <Grid item xs={12}>
          <TextField
            fullWidth
            label="Recipient Address"
            variant="outlined"
            value={recipient}
            onChange={e => setRecipient(e.target.value)}
          />
        </Grid>
        <Grid item xs={12}>
          <Button variant="contained" color="primary" onClick={shareFile}>Share File</Button>
        </Grid>
      </Grid>
    </Card>
  );
}

2. ViewSharedFiles Component:
After a user shares a file, we want the recipient to view the shared files. For this, we’ll create a ViewSharedFiles component.

import { Card, Typography } from "@material-ui/core";

export default function ViewSharedFiles() {
  const [sharedFiles, setSharedFiles] = useState([]);

  useEffect(() => {
    const fetchFiles = async () => {
      const files = await contract.methods.getSharedFiles().call({ from: account });
      setSharedFiles(files);
    }

    fetchFiles();
  }, []);

  return (
    <Card style={{ margin: "20px", padding: "20px" }}>
      <Typography variant="h5">Shared Files</Typography>
      {sharedFiles.map(file => (
        <Typography key={file}>{file}</Typography>
      ))}
    </Card>
  );
}

3. FileSection Component:
Our current design displays all files in a simple list. However, as users add more files, this might become unmanageable. Therefore, we will introduce a file organization system.
We can categorize files based on the type (e.g., Images, Documents, Others) and display them in separate sections on the user interface. Let’s create a FileSection component for this.

import { Card, Typography } from "@material-ui/core";

export default function FileSection({ title, files }) {
  return (
    <Card style={{ margin: "20px", padding: "20px" }}>
      <Typography variant="h5">{title}</Typography>
      {files.map(file => (
        <Typography key={file.hash}>{file.name}</Typography>
      ))}
    </Card>
  );
}

We can use this component in our main app and pass the appropriate files to each section.

// ... imports ...

export default function App() {
  // ... existing code ...

  const images = files.filter(file => file.type.startsWith('image/'));
  const documents = files.filter(file => file.type.startsWith('text/') || file.type === 'application/pdf');
  const others = files.filter(file => !images.includes(file) && !documents.includes(file));

  return (
    <div>
      // ... existing code ...
      <FileSection title="Images" files={images} />
      <FileSection title="Documents" files={documents} />
      <FileSection title="Others" files={others} />
    </div>
  );
}

Finally, In App.js, import the new components and incorporate them into the existing code:

import React, { useState, useEffect } from 'react';
import { Contract } from 'ethers';
import { Web3Provider } from '@ethersproject/providers';
import { create as ipfsClient } from 'ipfs-http-client';
import FileStorage from './FileStorage.json';
import { Buffer } from 'buffer';
import { Container, Grid, Paper, Typography, Button, TextField, Checkbox, FormControlLabel, makeStyles } from '@material-ui/core';
import FileViewer from 'react-file-viewer';
import './App.css';

import ShareFile from './ShareFile';  // New import
import ViewSharedFiles from './ViewSharedFiles';  // New import
import FileSection from './FileSection';  // New import

// ... existing code ...

function App() {
  // ... existing code ...

  const images = files.filter(file => file.type.startsWith('image/'));
  const documents = files.filter(file => file.type.startsWith('text/') || file.type === 'application/pdf');
  const others = files.filter(file => !images.includes(file) && !documents.includes(file));

  return (
    <Container>
      <Typography variant="h4" component="h1">Celo-based Decentralized Storage</Typography>
      <Grid container spacing={3}>
        <Grid item xs={12} sm={6}>
          <Paper className={classes.paper}>
            // ... existing code ...
          </Paper>
        </Grid>
        <Grid item xs={12} sm={6}>
          <Paper className={classes.paper}>
            // ... existing code ...
          </Paper>
        </Grid>
        <Grid item xs={12} sm={6}>
          <Paper className={classes.paper}>
            <ShareFile contract={fileStorage} account={/* Your account */} />
          </Paper>
        </Grid>
        <Grid item xs={12} sm={6}>
          <Paper className={classes.paper}>
            <ViewSharedFiles contract={fileStorage} account={/* Your account */} />
          </Paper>
        </Grid>
      </Grid>
      <div>
        <h2>File Preview</h2>
        {selectedFile && (
          <FileViewer
            fileType={selectedFile.type.split('/')[1]}
            filePath={URL.createObjectURL(selectedFile)}
          />
        )}
      </div>
      <FileSection title="Images" files={images} />
      <FileSection title="Documents" files={documents} />
      <FileSection title="Others" files={others} />
    </Container>
  );
}

export default App;

This updated code now includes the functionality to share files, view shared files, and also categorizes files based on type (images, documents, and others).
Remember to install Metamask in your browser and connect to it in order for your dApp to interact with the blockchain.

Testing the Application

After making these changes, you can start your application by running npm start in the terminal. Make sure your Metamask wallet is connected to the Celo test network.
You should now see the improved user interface. Try uploading different types of files and verify that they appear in the correct sections. Also, try the file-sharing feature by sharing a file with another account and checking that account’s shared files.

On running the command :
npm start

You should see this browser window with the application.

1. Uploading the File:

  • Click on choose File and select a file from your local storage

As you can see, we are able to see the preview of the file irrespective of its format.

  • Click on Upload File

  • Confirm the Transaction

The File is uploaded, the ipfs hash is recorded and tthis action is also registered on the celo blockchain.

2. Sharing the File:

The action is registered on the Celo Blockchain and the file is shared with the recipient account.

On switching to the receiving the address and refreshing the page, we can see the ipfs hash of the shared file, which can then be used for retrieval.

Conclusion

Congratulations! We have transformed our decentralized storage platform into a full-fledged file-sharing platform on the Celo blockchain. By adding new contract methods and creating a richer, more interactive user interface, we have developed an application that’s not just technically sound, but also user-friendly. This allows for secure p2p file sharing.

Next Steps

You have a strong foundation now, and there’s so much more you can do to enhance this application. Here are a few ideas:

  1. Implement file permissions: Allow the file owner to control who can view or download their files.
  2. Improve the file viewer: Add support for previewing more file types, like PDFs and videos.
  3. Enhance the user experience: Add user profiles, implement a search feature, or even enable users to comment on or like files.

Remember, learning is a journey. Keep experimenting, keep building, and most importantly, keep sharing your knowledge!

About the Author

Oluwalana is a blockchain developer and technical writer, experienced in creating decentralized applications on Ethereum and Celo platforms. With a passion for knowledge-sharing, Oluwalana has authored various tutorials and articles on blockchain and other emerging technologies. Follow me on Twitter for insights on blockchain and emerging tech. For professional inquiries, kindly connect witth me on LinkedIn and explore my work on GitHub.

References

  1. Tutorial Code Repo
  2. Celo Official Documentation
  3. Solidity Official Documentation
  4. React Official Documentation
  5. Truffle Suite Official Documentation
  6. Metamask Official Documentation
  7. Ethers.js Official Documentation
7 Likes

Looking forward to this :100:

3 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

4 Likes

Hi @Lanacreates
I’ll be reviewing your piece in 1 to 2 days

2 Likes

Hey! Nice job here, Before the reviewer comes, if I may ask, are you uploading the files directly to the blockchain?

5 Likes

The file is being uploaded directly to IPFS, but the metadata is being written directly to the blockchain.

The system can be used to securely share files in a decentralised manner. The part 1 does a good job of explaining how the data is stored.

Storing the entire file to the blockchain would cost too much in gas fees.

1 Like

Ok! That’s lovely. So are you using any of the IPFS storage?

4 Likes

Yes I am

1 Like

OK that’s great.

4 Likes