Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/server/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ type Entry {
creator: ID!
dateCreated: DateTime!
meta: JSON!
signedUrl: String!

"""Get the number of milliseconds the signed URL is valid for."""
signedUrlExpiration: Float!
}

type UploadSession {
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export default () => ({
},
dataset: {
prefix: process.env.GCP_STORAGE_DATASET_PREFIX || 'datasets'
},
entry: {
signedURLExpiration: process.env.GCP_STORAGE_ENTRY_SIGNED_URL_EXPIRATION || (15 * 60 * 1000) // 15 minutes
}
});
3 changes: 3 additions & 0 deletions packages/server/src/entry/models/entry.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export class Entry {
@Field(() => JSON)
meta: any;

@Prop({ required: false })
signedURLExpiration: Date;

// TODO: Add creator field
}

Expand Down
16 changes: 14 additions & 2 deletions packages/server/src/entry/resolvers/entry.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Args, ID, Mutation, Resolver, Query } from '@nestjs/graphql';
import { Args, ID, Mutation, Resolver, Query, ResolveField, Parent } from '@nestjs/graphql';
import { Dataset } from '../../dataset/dataset.model';
import { Entry } from '../models/entry.model';
import { EntryCreate } from '../dtos/create.dto';
Expand All @@ -16,7 +16,19 @@ export class EntryResolver {
}

@Query(() => [Entry])
async entryForDataset(@Args('dataset', { type: () => ID }) dataset: Dataset): Promise<Entry[]> {
async entryForDataset(@Args('dataset', { type: () => ID }, DatasetPipe) dataset: Dataset): Promise<Entry[]> {
return this.entryService.findForDataset(dataset);
}

@ResolveField(() => String)
async signedUrl(@Parent() entry: Entry): Promise<string> {
return this.entryService.getSignedUrl(entry);
}

// NOTE: With the current implementation, this is only really helpful
// if the request to `signedUrl` is made.
@ResolveField(() => Number, { description: 'Get the number of milliseconds the signed URL is valid for.' })
async signedUrlExpiration(@Parent() entry: Entry): Promise<number> {
return this.entryService.getSignedUrlExpiration(entry);
}
}
33 changes: 30 additions & 3 deletions packages/server/src/entry/services/entry.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Inject } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Entry } from '../models/entry.model';
import { Model } from 'mongoose';
import { EntryCreate } from '../dtos/create.dto';
import { Dataset } from '../../dataset/dataset.model';
import { GCP_STORAGE_PROVIDER } from '../../gcp/providers/storage.provider';
import { Bucket, Storage } from '@google-cloud/storage';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class EntryService {
constructor(@InjectModel(Entry.name) private readonly entryMode: Model<Entry>) {}
private readonly bucketName = this.configService.getOrThrow<string>('gcp.storage.bucket');
private readonly bucket: Bucket = this.storage.bucket(this.bucketName);
private readonly expiration = this.configService.getOrThrow<number>('entry.signedURLExpiration');

constructor(@InjectModel(Entry.name) private readonly entryMode: Model<Entry>,
@Inject(GCP_STORAGE_PROVIDER) private readonly storage: Storage,
private readonly configService: ConfigService) {}

async find(entryID: string): Promise<Entry | null> {
return this.entryMode.findOne({ _id: entryID });
Expand All @@ -23,7 +32,7 @@ export class EntryService {
}

async findForDataset(dataset: Dataset): Promise<Entry[]> {
return this.entryMode.find({ dataset: dataset._id });
return this.entryMode.find({ dataset: dataset._id.toString() });
}

async exists(entryID: string, dataset: Dataset): Promise<boolean> {
Expand All @@ -34,4 +43,22 @@ export class EntryService {
async setBucketLocation(entry: Entry, bucketLocation: string): Promise<void> {
await this.entryMode.updateOne({ _id: entry._id }, { bucketLocation });
}

async getSignedUrl(entry: Entry): Promise<string> {
const file = this.bucket.file(entry.bucketLocation);
const [url] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + this.expiration
});
return url;
}

/**
* Get how long the signed URL is valid for in milliseconds.
*
* In the future, this could be configurable per entry.
*/
async getSignedUrlExpiration(_entry: Entry): Promise<number> {
return this.expiration;
}
}