Skip to content

Commit

Permalink
api,ui:documents stored in minio
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Baus committed Feb 10, 2021
1 parent 1bc6832 commit a047617
Show file tree
Hide file tree
Showing 30 changed files with 945 additions and 183 deletions.
392 changes: 231 additions & 161 deletions api/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"joi": "^14.3.1",
"jsonwebtoken": "^8.5.0",
"lodash.isequal": "^4.5.0",
"minio": "^7.0.17",
"pino": "^5.8.0",
"pino-pretty": "^2.2.3",
"raw-body": "^2.3.3",
Expand All @@ -90,6 +91,7 @@
"@types/joi": "^14.3.2",
"@types/jsonwebtoken": "^8.0.0",
"@types/lodash.isequal": "^4.5.5",
"@types/minio": "^7.0.6",
"@types/mocha": "^5.2.6",
"@types/node": "^14.6.4",
"@types/pino": "^6.3.0",
Expand Down
10 changes: 10 additions & 0 deletions api/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const host = process.env.API_HOST || "master-api";
export const port = process.env.PORT || 8080;
export const isSsl = process.env.USE_SSL === "ssl" ? true : false;
export const hostPort = `${isSsl ? "https" : "http"}://${host}:${port}`;

export const minioEndPoint = process.env.MINIO_ENDPOINT; // nginx in development
export const minioPort = process.env.MINIO_PORT && parseInt(process.env.MINIO_PORT as string, 10) || 9000;
export const minioUseSSL = process.env.MINIO_USE_SSL === "true" ? true : false;
export const minioAccessKey = process.env.MINIO_ACCESS_KEY || "minio";
export const minioSecretKey = process.env.MINIO_SECRET_KEY || "minio123";
17 changes: 17 additions & 0 deletions api/src/http_errors/not_found.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const schema = {
description: "Not found",
type: "object",
properties: {
apiVersion: { type: "string", example: "1.0" },
error: {
type: "object",
properties: {
code: { type: "string", example: "404" },
message: {
type: "string",
example: "The route you are looking for was not found.",
},
},
},
},
};
2 changes: 1 addition & 1 deletion api/src/httpd/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const createBasicApp = (

server.addContentTypeParser("application/gzip", async function (request, payload) {
request.headers["content-length"] = "1024mb";
return payload;
return payload;
});

// app.use(logging);
Expand Down
15 changes: 15 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import * as WorkflowitemAssignService from "./service/workflowitem_assign";
import * as WorkflowitemCloseService from "./service/workflowitem_close";
import * as WorkflowitemCreateService from "./service/workflowitem_create";
import * as WorkflowitemDocumentDownloadService from "./service/workflowitem_document_download";
import * as WorkflowitemDocumentDownloadMinioService from "./service/workflowitem_document_minio_download";
import * as WorkflowitemGetService from "./service/workflowitem_get";
import * as WorkflowitemViewHistoryService from "./service/workflowitem_history_get";
import * as WorkflowitemListService from "./service/workflowitem_list";
Expand Down Expand Up @@ -119,6 +120,7 @@ import * as WorkflowitemAssignAPI from "./workflowitem_assign";
import * as WorkflowitemCloseAPI from "./workflowitem_close";
import * as WorkflowitemCreateAPI from "./workflowitem_create";
import * as WorkflowitemsDocumentDownloadAPI from "./workflowitem_download_document";
import * as WorkflowitemsDocumentDownloadMinioAPI from "./workflowitem_download_document_minio";
import * as WorkflowitemListAPI from "./workflowitem_list";
import * as WorkflowitemPermissionGrantAPI from "./workflowitem_permission_grant";
import * as WorkflowitemPermissionRevokeAPI from "./workflowitem_permission_revoke";
Expand Down Expand Up @@ -159,6 +161,7 @@ if (!organizationVaultSecret) {

const SWAGGER_BASEPATH = process.env.SWAGGER_BASEPATH || "/";


/*
* Initialize the components:
*/
Expand Down Expand Up @@ -774,6 +777,18 @@ WorkflowitemsDocumentDownloadAPI.addHttpHandler(server, URL_PREFIX, {
),
});

WorkflowitemsDocumentDownloadMinioAPI.addHttpHandler(server, URL_PREFIX, {
getDocumentMinio: (ctx, projectId, subprojectId, workflowitemId, documentId) =>
WorkflowitemDocumentDownloadMinioService.getDocumentMinio(
db,
ctx,
projectId,
subprojectId,
workflowitemId,
documentId,
),
});

/*
* Run the server.
*/
Expand Down
136 changes: 136 additions & 0 deletions api/src/lib/minio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as Minio from "minio";
import { v4 as uuid } from "uuid";
import { minioEndPoint, minioPort, minioUseSSL, minioAccessKey, minioSecretKey } from "../config";

const Readable = require("stream").Readable;

interface Metadata {
"Content-Type"?: string,
fileName: string,
}

interface MetadataWithName extends Metadata {
name: string
}

const minioClient: any = new Minio.Client({
endPoint: minioEndPoint || "nginx",
port: minioPort,
useSSL: minioUseSSL,
accessKey: minioAccessKey,
secretKey: minioSecretKey,
});

const bucketName: string = "trubudget";


const makeBucket = (bucket: string, cb: Function) => {
minioClient.bucketExists(bucket, (err, exists) => {
if (err) {
return console.error("Error during searching for bucket", err);
}

if (!exists) {
minioClient.makeBucket(bucket, "us-east-1", (err) => {
if (err) {
console.error("Error creating bucket.", err);
return cb(err);
}
console.log(`Minio: Bucket ${bucket} created.`);
return cb(null, true);
});
}
});
};

export const makeBucketAsPromised = (bucket: string) => {
return new Promise((resolve, reject) => {
makeBucket(bucket, (err) => {
if (err) return reject(err);

resolve(true);
});
});
};


const upload = (file: string, content: string, metaData: Metadata, cb: Function) => {
const s = new Readable();
s._read = () => {};
s.push(content);
s.push(null);

const metaDataWithName: MetadataWithName = { ...metaData, name: file };
// Using putObject API upload your file to the bucket .
minioClient.putObject(bucketName, file, s, metaDataWithName, (err, etag) => {
if (err) {
console.error("minioClient.putObject", err);
return cb(err);
}

return cb(null, etag);
});
};

export const uploadAsPromised = (file: string, content: string, metaData: Metadata = {fileName: "default"}) => {
return new Promise((resolve, reject) => {
upload(file, content, metaData, (err, etag) => {
if (err) return reject(err);

resolve(etag);
});
});
};

const download = (file: string, cb: Function) => {
let fileContent: string = "";
minioClient.getObject(bucketName, file, (err, dataStream) => {
if (err) {
console.error("Error during getting file object", err);
cb(err);
}
dataStream.on("data", (chunk: string) => {
fileContent += chunk;
});
dataStream.on("end", () => {
cb(null, fileContent);
});
dataStream.on("error", function (err) {
console.error("Error during getting file object datastream", err);
});
});
};

export const downloadAsPromised = (file: string) => {
return new Promise((resolve, reject) => {
download(file, (err, fileContent: string) => {
if (err) return reject(err);

resolve(fileContent);
});
});
};

const getMetadata = (fileHash: string, cb: Function) => {
minioClient.statObject(bucketName, fileHash, (err, stat: MetadataWithName) => {
if (err) {
console.error(err);
return cb(err);
}
cb(null, stat);
});
};

export const getMetadataAsPromised = (fileHash: string) => {
return new Promise((resolve, reject) => {
getMetadata(fileHash, (err, metaData: MetadataWithName) => {
if (err) return reject(err);

resolve(metaData);
});
});
};

makeBucketAsPromised(bucketName);

export default minioClient;
2 changes: 2 additions & 0 deletions api/src/service/domain/workflow/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface UploadedDocument {
id: string;
base64: string;
fileName: string;
url?: string;
}

export const uploadedDocumentSchema = Joi.object({
Expand All @@ -30,6 +31,7 @@ export const uploadedDocumentSchema = Joi.object({
.required()
.error(() => new Error("Document can't be an empty file")),
fileName: Joi.string(),
orgAccess: Joi.array().items(Joi.string()).optional(),
});

export async function hashDocument(
Expand Down
2 changes: 2 additions & 0 deletions api/src/service/domain/workflow/workflowitem_create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe("Create workflowitem", () => {
workflowitemExists: async (_projectId, _subprojectId, _workflowitemId) => false,
getSubproject: async () => baseSubproject,
applyWorkflowitemType: () => [],
uploadDocument: () => new Promise(() => undefined),
});

assert.isTrue(Result.isErr(result));
Expand All @@ -61,6 +62,7 @@ describe("Create workflowitem", () => {
workflowitemExists: async (_projectId, _subprojectId, _workflowitemId) => false,
getSubproject: async () => baseSubproject,
applyWorkflowitemType: () => [],
uploadDocument: () => new Promise(() => undefined),
});

assert.isTrue(Result.isErr(result));
Expand Down
15 changes: 14 additions & 1 deletion api/src/service/domain/workflow/workflowitem_create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Joi = require("joi");
import { VError } from "verror";
import { minioEndPoint, hostPort } from "../../../config";
import Intent, { workflowitemIntents } from "../../../authz/intents";
import { Ctx } from "../../../lib/ctx";
import * as Result from "../../../result";
Expand Down Expand Up @@ -77,6 +78,9 @@ interface Repository {
event: BusinessEvent,
workflowitem: Workflowitem.Workflowitem,
): Result.Type<BusinessEvent[]>;
uploadDocument(
document: UploadedDocument
): Promise<void>;
}

export async function createWorkflowitem(
Expand Down Expand Up @@ -199,7 +203,16 @@ export async function createWorkflowitem(
if (Result.isErr(result)) {
return result;
}
documentUploadedEvents.push(result);
const { document } = result as WorkflowitemDocumentUploaded.Event;
// document should be private
if (minioEndPoint) {
await repository.uploadDocument(document);
const eventData = {...result, document: {...document, base64: "", url: hostPort}};
documentUploadedEvents.push(eventData);
} else {
documentUploadedEvents.push(result);
}

}

// Check the workflowitem type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ServiceUser } from "../organization/service_user";
import * as WorkflowitemDocument from "./document";
import * as Workflowitem from "./workflowitem";
import * as WorkflowitemDocumentUploaded from "./workflowitem_document_uploaded";
import { getDocument as getDocumentService } from "../../workflowitem_document_download";
import VError = require("verror");

interface Repository {
Expand All @@ -31,7 +32,10 @@ export async function getDocument(
return new NotAuthorized({ ctx, userId: user.id, intent, target: workflowitem });
}

// Get all events from one document
/**
* Get all events from one document
* @see getDocumentService
*/
const documentEvents = await repository.getDocumentEvents(documentId);
if (Result.isErr(documentEvents)) {
return new VError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Ctx } from "../../../lib/ctx";
import * as Result from "../../../result";
import { NotAuthorized } from "../errors/not_authorized";
import { NotFound } from "../errors/not_found";
import { ServiceUser } from "../organization/service_user";
import * as WorkflowitemDocument from "./document";
import * as Workflowitem from "./workflowitem";
import * as WorkflowitemDocumentUploaded from "./workflowitem_document_uploaded";
import VError = require("verror");

interface Repository {
getWorkflowitem(workflowitemId): Promise<Result.Type<Workflowitem.Workflowitem>>;
getDocumentEvents(documentId): Promise<Result.Type<WorkflowitemDocumentUploaded.Event[]>>;
}

export async function getDocumentMinio(
ctx: Ctx,
workflowitemId: string,
documentId: string,
repository: Repository,
): Promise<Result.Type<WorkflowitemDocument.UploadedDocument>> {
// check for permissions etc
const workflowitem = await repository.getWorkflowitem(workflowitemId);
if (Result.isErr(workflowitem)) {
return workflowitem;
}

// Get all events from one document
const documentEvents = await repository.getDocumentEvents(documentId);
if (Result.isErr(documentEvents)) {
return new VError(
new NotFound(ctx, "document", documentId),
`couldn't get document events from ${workflowitem}`,
);
}

// Only return if document has relation to the workflowitem
if (!workflowitem.documents.some((d) => d.documentId === documentId)) {
return new VError(
new NotFound(ctx, "document", documentId),
`workfowitem ${workflowitem} has no link to document`,
);
}

return documentEvents
.filter((d) => d.workflowitemId === workflowitem.id)
.map((d) => d.document)[0];
}
5 changes: 4 additions & 1 deletion api/src/service/domain/workflow/workflowitem_update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { BusinessEvent } from "../business_event";
import { NotAuthorized } from "../errors/not_authorized";
import { NotFound } from "../errors/not_found";
import { ServiceUser } from "../organization/service_user";
import { hashDocument, StoredDocument } from "./document";
import { hashDocument, StoredDocument, UploadedDocument } from "./document";
import { Workflowitem } from "./workflowitem";
import { updateWorkflowitem } from "./workflowitem_update";
import * as Nodes from "../../../network/model/Nodes";

const ctx: Ctx = { requestId: "", source: "test" };
const root: ServiceUser = { id: "root", groups: [] };
Expand Down Expand Up @@ -51,6 +52,8 @@ const baseRepository = {
if (identity === "root") return ["root"];
throw Error(`unexpected identity: ${identity}`);
},
uploadDocument: (document: UploadedDocument): Promise<void> => { return new Promise((resolve) => resolve(undefined)); },
getOrganizations: (): Promise<Nodes.NodeInfo[]> => { return new Promise((resolve) => resolve([])); },
};

describe("update workflowitem: authorization", () => {
Expand Down
Loading

0 comments on commit a047617

Please sign in to comment.