Skip to content
This repository has been archived by the owner on May 19, 2023. It is now read-only.

Commit

Permalink
Implement backup per did feature (#53)
Browse files Browse the repository at this point in the history
* Provider layer

* Service layer

* Client layer

* Use authManager to getBackup
  • Loading branch information
javiesses authored Dec 29, 2020
1 parent cc004e9 commit c861971
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 3 deletions.
13 changes: 13 additions & 0 deletions modules/ipfs-cpinner-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ console.log(`Used: ${storage.used}`)
console.log(`Available: ${storage.available}`)
```

### Get backup information

```typescript
import DataVaultWebClient from '@rsksmart/ipfs-cpinner-client'

const client = new DataVaultWebClient({ serviceUrl, did, rpcPersonalSign, serviceDid })

const backup = await client.getBackup()

console.log('This is the keys and cids you have stored in the DV')
console.log(backup)
```

### Create

```typescript
Expand Down
14 changes: 13 additions & 1 deletion modules/ipfs-cpinner-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AUTHENTICATION_ERROR, MAX_STORAGE_REACHED, SERVICE_MAX_STORAGE_REACHED,
import {
CreateContentPayload, CreateContentResponse,
DeleteTokenPayload, GetContentPayload, Config,
SwapContentPayload, SwapContentResponse, GetContentResponsePayload, StorageInformation
SwapContentPayload, SwapContentResponse, GetContentResponsePayload, StorageInformation, Backup
} from './types'

export default class {
Expand Down Expand Up @@ -56,6 +56,18 @@ export default class {
.catch(this.errorHandler)
}

getBackup (): Promise<Backup> {
const { serviceUrl } = this.config

return this.authManager.getAccessToken()
.then(accessToken => axios.get(
`${serviceUrl}/backup`,
{ headers: { Authorization: `DIDAuth ${accessToken}` } })
)
.then(res => res.status === 200 && res.data)
.catch(this.errorHandler)
}

create (payload: CreateContentPayload): Promise<CreateContentResponse> {
const { content, key } = payload
const { serviceUrl } = this.config
Expand Down
1 change: 1 addition & 0 deletions modules/ipfs-cpinner-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type DeleteTokenPayload = { key: string, id?: string }
export type SwapContentPayload = { key: string, content: string, id?: string }
export type SwapContentResponse = { id: string }
export type StorageInformation = { used: number, available: number }
export type Backup = { key: string, id: string }[]

export type Config = {
serviceUrl: string
Expand Down
66 changes: 66 additions & 0 deletions modules/ipfs-cpinner-client/test/get-backup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { IpfsPinnerProvider } from '@rsksmart/ipfs-cpinner-provider'
import { deleteDatabase, resetDatabase, setupDataVaultClient, startService, testTimestamp } from './util'
import { Server } from 'http'
import { Connection } from 'typeorm'
import MockDate from 'mockdate'
import localStorageMockFactory from './localStorageMockFactory'

describe('get backup', function (this: {
server: Server,
dbConnection: Connection,
ipfsPinnerProvider: IpfsPinnerProvider,
serviceUrl: string,
serviceDid: string
}) {
const dbName = 'get-backup.sqlite'

beforeAll(async () => {
const { server, serviceUrl, ipfsPinnerProvider, dbConnection, serviceDid } = await startService(dbName, 4608)
this.server = server
this.ipfsPinnerProvider = ipfsPinnerProvider
this.dbConnection = dbConnection
this.serviceUrl = serviceUrl
this.serviceDid = serviceDid
})

afterAll(async () => {
this.server.close()
await deleteDatabase(this.dbConnection, dbName)
})

beforeEach(() => {
MockDate.set(testTimestamp)
global.localStorage = localStorageMockFactory()
})

afterEach(async () => {
MockDate.reset()
await resetDatabase(this.dbConnection)
})

test('should return an empty object if no data saved', async () => {
const { dataVaultClient } = await setupDataVaultClient(this.serviceUrl, this.serviceDid)

const backup = await dataVaultClient.getBackup()

expect(backup).toEqual([])
})

test('should return the saved data', async () => {
const { dataVaultClient, did } = await setupDataVaultClient(this.serviceUrl, this.serviceDid)

const key1 = 'TheKey1'
const key2 = 'TheKey2'
const id1 = await this.ipfsPinnerProvider.create(did, key1, 'some content')
const id2 = await this.ipfsPinnerProvider.create(did, key1, 'another content for same did')
const id3 = await this.ipfsPinnerProvider.create(did, key2, 'another content for another did')

const backup = await dataVaultClient.getBackup()

expect(backup).toEqual([
{ key: key1, id: id1 },
{ key: key1, id: id2 },
{ key: key2, id: id3 }
])
})
})
2 changes: 2 additions & 0 deletions modules/ipfs-cpinner-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ const deleted: boolean = await ipfsPinnerProvider.delete(did, key, cid) // cid c
const availableStorage: number = await ipfsPinnerProvider.getAvailableStorage(did) // return the amount of bytes available to store value associated to the given did
const usedStorage: number = await ipfsPinnerProvider.getUsedStorage(did) // return the amount of bytes used to store value associated to the given did
const didBackup: Backup = await ipfsPinnerProvider.getBackup(did) // return an array containing all the keys and cids created by the given did
```

See our [documentation](https://developers.rsk.co/rif/identity/) for advanced usage.
Expand Down
6 changes: 5 additions & 1 deletion modules/ipfs-cpinner-provider/src/IpfsPinnerProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MAX_STORAGE_REACHED } from './constants'
import {
Content, CID, DID, IpfsPinner, Key, MetadataManager, IpfsClient, SavedContent
Content, CID, DID, IpfsPinner, Key, MetadataManager, IpfsClient, SavedContent, Backup
} from './types'

export default class IpfsPinnerProvider {
Expand Down Expand Up @@ -81,6 +81,10 @@ export default class IpfsPinnerProvider {
return this.metadataManager.getUsedStorage(did)
}

getBackup (did: DID): Promise<Backup> {
return this.metadataManager.getBackupByDid(did)
}

private async deleteByCid (did: DID, key: Key, cid: CID): Promise<boolean> {
const removed = await this.metadataManager.delete(did, key, cid)
if (removed) return this.ipfsPinner.unpin(cid)
Expand Down
9 changes: 8 additions & 1 deletion modules/ipfs-cpinner-provider/src/MetadataManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Repository } from 'typeorm'
import IpfsMetadata from './entities/ipfs-metadata'
import { CID, MetadataManager, DID, Key } from './types'
import { CID, MetadataManager, DID, Key, Backup } from './types'

export default class implements MetadataManager {
// eslint-disable-next-line no-useless-constructor
Expand Down Expand Up @@ -53,4 +53,11 @@ export default class implements MetadataManager {
select: ['contentSize']
}).then((entries: { contentSize: number }[]) => entries.map(e => e.contentSize).reduce((a, b) => a + b, 0))
}

getBackupByDid (did: string): Promise<Backup> {
return this.repository.find({
where: { did },
select: ['key', 'cid']
}).then((entries: IpfsMetadata[]) => entries.map(({ key, cid }) => ({ key, id: cid })))
}
}
2 changes: 2 additions & 0 deletions modules/ipfs-cpinner-provider/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type CID = string
export type Content = string

export type SavedContent = { id: CID, content: Content }
export type Backup = { key: Key, id: CID }[]

export interface MetadataManager {
save(did: DID, key: Key, id: CID, contentSize: number): Promise<boolean>
Expand All @@ -14,6 +15,7 @@ export interface MetadataManager {
getKeys (did: DID): Promise<Key[]>
getUsedStorage (did: DID): Promise<number>
getUsedStorageByDidKeyAndCid (did: DID, key: Key, cid: CID): Promise<number>
getBackupByDid (did: DID): Promise<Backup>
}

export interface IpfsClient {
Expand Down
11 changes: 11 additions & 0 deletions modules/ipfs-cpinner-provider/test/ipfsPinnerProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,15 @@ describe('ipfs pinner provider', function (this: {

expect(keys).toEqual([key, anotherKey])
})

test('get backup with more than one key and content', async () => {
const anotherKey = 'another key'

const id1 = await this.centralizedPinnerProvider.create(did, key, content)
const id2 = await this.centralizedPinnerProvider.create(did, anotherKey, content)

const data = await this.centralizedPinnerProvider.getBackup(did)

expect(data).toEqual([{ key, id: id1 }, { key: anotherKey, id: id2 }])
})
})
21 changes: 21 additions & 0 deletions modules/ipfs-cpinner-provider/test/metadataManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,25 @@ describe('metadata manager', function (this: {
const totalUsedStorage = await this.metadataManager.getUsedStorage(did)
expect(totalUsedStorage).toEqual(contentSize + anotherContentSize)
})

test('should get empty array when getting a backup if no content created', async () => {
const data = await this.metadataManager.getBackupByDid(did)

expect(data).toEqual([])
})

test('should get an array containing created data when requesting the backup', async () => {
await this.metadataManager.save(did, key, cid, contentSize)

const data = await this.metadataManager.getBackupByDid(did)
expect(data).toEqual([{ key, id: cid }])
})

test('should get an array with repeated data if it has been saved twice', async () => {
await this.metadataManager.save(did, key, cid, contentSize)
await this.metadataManager.save(did, key, cid, contentSize)

const data = await this.metadataManager.getBackupByDid(did)
expect(data).toEqual([{ key, id: cid }, { key, id: cid }])
})
})
6 changes: 6 additions & 0 deletions modules/ipfs-cpinner-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ Get storage availability information
GET /storage -> { used: number, available: number }
```

Get `key: cid` backup of the logged did (needs authentication)

```
GET /backup -> { key: string, id: string }[]
```

## Advanced usage

See our [documentation](https://rsksmart.github.io/rif-identity-docs/data-vault/cpinner/cpinner-service)
Expand Down
10 changes: 10 additions & 0 deletions modules/ipfs-cpinner-service/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ export function setupPermissionedApi (app: Express, provider: IpfsPinnerProvider
.catch(err => errorHandler(err, req, res))
})

app.get('/backup', async (req: AuthenticatedRequest, res) => {
const { did } = req.user

logger.info(`Retrieving backup from ${did}`)

return provider.getBackup(did)
.then(backup => res.status(200).json(backup))
.catch(err => errorHandler(err, req, res))
})

app.post('/content/:key', (req: AuthenticatedRequest, res: Response) => {
const { did } = req.user
const { key } = req.params
Expand Down
73 changes: 73 additions & 0 deletions modules/ipfs-cpinner-service/test/api.permissioned.backup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Connection } from 'typeorm'
import express, { Express } from 'express'
import bodyParser from 'body-parser'
import { IpfsPinnerProvider, ipfsPinnerProviderFactory } from '@rsksmart/ipfs-cpinner-provider'
import { createSqliteConnection, deleteDatabase, ipfsApiUrl, mockedLogger } from './util'
import { setupPermissionedApi } from '../src/api'
import request from 'supertest'

describe('backup', function (this: {
app: Express,
did: string
dbConnection: Connection,
dbName: string,
provider: IpfsPinnerProvider
}) {
const maxStorage = 1000

const setup = async () => {
this.dbConnection = await createSqliteConnection(this.dbName)
this.provider = await ipfsPinnerProviderFactory({ dbConnection: this.dbConnection, ipfsApiUrl, maxStorage })

setupPermissionedApi(this.app, this.provider, mockedLogger)
}

beforeEach(() => {
this.did = 'did:ethr:rsk:testnet:0xce83da2a364f37e44ec1a17f7f453a5e24395c70'
const middleware = (req, res, next) => {
req.user = { did: this.did }
next()
}
this.app = express()
this.app.use(bodyParser.json())
this.app.use(middleware)
})

afterEach(() => deleteDatabase(this.dbConnection, this.dbName))

it('should return an empty array if nothing created', async () => {
this.dbName = 'backup-1.sqlite'
await setup()

const { body } = await request(this.app).get('/backup').expect(200)

expect(body).toEqual([])
})

it('should return an array with the just created content', async () => {
this.dbName = 'backup-2.sqlite'
await setup()

const key = 'TheKey'
const id = await this.provider.create(this.did, key, 'a content')

const { body } = await request(this.app).get('/backup').expect(200)

expect(body).toEqual([{ key, id }])
})

it('should return information related to the logged did even if there is data related to other did', async () => {
this.dbName = 'backup-3.sqlite'
await setup()

const anotherDid = 'did:ethr:rsk:testnet:0xce83da2a364f37e44ec1a17f7f453a5e24395c65'
await this.provider.create(anotherDid, 'AnotherKey', 'a content')

const key = 'TheKey'
const id = await this.provider.create(this.did, key, 'another content')

const { body } = await request(this.app).get('/backup').expect(200)

expect(body).toEqual([{ key, id }])
})
})

0 comments on commit c861971

Please sign in to comment.