diff --git a/.circleci/build.sh b/.circleci/build.sh index de1722e35..ec4b135a6 100755 --- a/.circleci/build.sh +++ b/.circleci/build.sh @@ -12,6 +12,8 @@ DOCKER_IMAGE_TAG=speckle/speckle-$SPECKLE_SERVER_PACKAGE IMAGE_VERSION_TAG="${IMAGE_VERSION_TAG:-0}" echo $IMAGE_VERSION_TAG +export DOCKER_BUILDKIT=1 + docker build --build-arg SPECKLE_SERVER_VERSION=$IMAGE_VERSION_TAG -t $DOCKER_IMAGE_TAG:latest . -f $FOLDER/$SPECKLE_SERVER_PACKAGE/Dockerfile docker tag $DOCKER_IMAGE_TAG:latest $DOCKER_IMAGE_TAG:$IMAGE_VERSION_TAG diff --git a/.circleci/config.yml b/.circleci/config.yml index bdd82cc24..607939b30 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -179,6 +179,7 @@ jobs: docker-build-and-publish: &docker-job docker: &docker-image - image: cimg/node:16.15 + resource_class: xlarge working_directory: *work-dir steps: - checkout diff --git a/docker-compose-speckle.yml b/docker-compose-speckle.yml index 757559624..a090ccf17 100644 --- a/docker-compose-speckle.yml +++ b/docker-compose-speckle.yml @@ -8,6 +8,8 @@ services: restart: always ports: - '0.0.0.0:80:80' + environment: + FILE_SIZE_LIMIT_MB: 100 speckle-server: build: @@ -37,6 +39,7 @@ services: S3_SECRET_KEY: 'minioadmin' S3_BUCKET: 'speckle-server' S3_CREATE_BUCKET: 'true' + FILE_SIZE_LIMIT_MB: 100 preview-service: build: diff --git a/packages/fileimport-service/src/daemon.js b/packages/fileimport-service/src/daemon.js index edca7c66b..320fc108f 100644 --- a/packages/fileimport-service/src/daemon.js +++ b/packages/fileimport-service/src/daemon.js @@ -25,6 +25,11 @@ const TMP_RESULTS_PATH = '/tmp/import_result.json' let shouldExit = false +let TIME_LIMIT = 10 * 60 * 1000 + +const providedTimeLimit = parseInt(process.env.FILE_IMPORT_TIME_LIMIT_MIN) +if (providedTimeLimit) TIME_LIMIT = providedTimeLimit * 60 * 1000 + async function startTask() { const { rows } = await knex.raw(` UPDATE file_uploads @@ -91,7 +96,7 @@ async function doTask(task) { { USER_TOKEN: tempUserToken }, - 20 * 60 * 1000 + TIME_LIMIT ) } else if (info.fileType === 'stl') { await runProcessWithTimeout( @@ -107,7 +112,7 @@ async function doTask(task) { { USER_TOKEN: tempUserToken }, - 10 * 60 * 1000 + TIME_LIMIT ) } else if (info.fileType === 'obj') { await objDependencies.downloadDependencies({ @@ -131,7 +136,7 @@ async function doTask(task) { { USER_TOKEN: tempUserToken }, - 10 * 60 * 1000 + TIME_LIMIT ) } else { throw new Error(`File type ${info.fileType} is not supported`) diff --git a/packages/frontend/Dockerfile b/packages/frontend/Dockerfile index de2e741d5..bee06981a 100644 --- a/packages/frontend/Dockerfile +++ b/packages/frontend/Dockerfile @@ -3,6 +3,7 @@ # build stage FROM node:16.15-bullseye-slim as build-stage ARG SPECKLE_SERVER_VERSION=custom + WORKDIR /speckle-server COPY .yarnrc.yml . @@ -25,9 +26,16 @@ COPY packages/frontend ./packages/frontend/ RUN yarn workspaces foreach -pt run build # production stage -FROM openresty/openresty:1.19.9.1-bullseye as production-stage + + +FROM openresty/openresty:1.21.4.1-bullseye as production-stage COPY --from=build-stage /speckle-server/packages/frontend/dist /usr/share/nginx/html RUN rm /etc/nginx/conf.d/default.conf -COPY packages/frontend/nginx/nginx.conf /etc/nginx/conf.d + +COPY packages/frontend/nginx/ /etc/nginx/ + +# prepare the environment +ENTRYPOINT ["/etc/nginx/docker-entrypoint.sh"] + EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] diff --git a/packages/frontend/nginx/docker-entrypoint.sh b/packages/frontend/nginx/docker-entrypoint.sh new file mode 100755 index 000000000..00fada557 --- /dev/null +++ b/packages/frontend/nginx/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu pipefail +defined_envs=$(printf '${%s} ' $(env | cut -d= -f1)) + +echo Starting nginx environment template rendering with $defined_envs + +envsubst "$defined_envs" < /etc/nginx/templates/nginx.conf.template > /etc/nginx/conf.d/nginx.conf + +echo Nginx conf rendered, starting server... +exec "$@" diff --git a/packages/frontend/nginx/nginx.conf b/packages/frontend/nginx/templates/nginx.conf.template similarity index 98% rename from packages/frontend/nginx/nginx.conf rename to packages/frontend/nginx/templates/nginx.conf.template index 64606411a..4cd2981e9 100644 --- a/packages/frontend/nginx/nginx.conf +++ b/packages/frontend/nginx/templates/nginx.conf.template @@ -110,7 +110,7 @@ server { location ~* ^/(graphql|explorer|(auth/.*)|(objects/.*)|(preview/.*)|(api/.*)) { resolver 127.0.0.11 valid=30s; set $upstream_speckle_server speckle-server; - client_max_body_size 100m; + client_max_body_size ${FILE_SIZE_LIMIT_MB}m; proxy_pass http://$upstream_speckle_server:3000; proxy_buffering off; diff --git a/packages/frontend/src/graphql/generated/graphql.ts b/packages/frontend/src/graphql/generated/graphql.ts index c549df551..4f6063948 100644 --- a/packages/frontend/src/graphql/generated/graphql.ts +++ b/packages/frontend/src/graphql/generated/graphql.ts @@ -991,6 +991,7 @@ export type ServerInfo = { adminContact?: Maybe; /** The authentication strategies available on this server. */ authStrategies?: Maybe>>; + blobSizeLimitBytes: Scalars['Int']; canonicalUrl?: Maybe; company?: Maybe; description?: Maybe; @@ -1607,6 +1608,8 @@ export type StreamObjectNoDataQueryVariables = Exact<{ export type StreamObjectNoDataQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, object?: { __typename?: 'Object', totalChildrenCount?: number | null, id: string, speckleType?: string | null } | null } | null }; +export type ServerInfoBlobSizeFieldsFragment = { __typename?: 'ServerInfo', blobSizeLimitBytes: number }; + export type MainServerInfoFieldsFragment = { __typename?: 'ServerInfo', name: string, company?: string | null, description?: string | null, adminContact?: string | null, canonicalUrl?: string | null, termsOfService?: string | null, inviteOnly?: boolean | null, version?: string | null }; export type ServerInfoRolesFieldsFragment = { __typename?: 'ServerInfo', roles: Array<{ __typename?: 'Role', name: string, description: string, resourceTarget: string } | null> }; @@ -1621,7 +1624,12 @@ export type MainServerInfoQuery = { __typename?: 'Query', serverInfo: { __typena export type FullServerInfoQueryVariables = Exact<{ [key: string]: never; }>; -export type FullServerInfoQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', name: string, company?: string | null, description?: string | null, adminContact?: string | null, canonicalUrl?: string | null, termsOfService?: string | null, inviteOnly?: boolean | null, version?: string | null, roles: Array<{ __typename?: 'Role', name: string, description: string, resourceTarget: string } | null>, scopes: Array<{ __typename?: 'Scope', name: string, description: string } | null> } }; +export type FullServerInfoQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', name: string, company?: string | null, description?: string | null, adminContact?: string | null, canonicalUrl?: string | null, termsOfService?: string | null, inviteOnly?: boolean | null, version?: string | null, blobSizeLimitBytes: number, roles: Array<{ __typename?: 'Role', name: string, description: string, resourceTarget: string } | null>, scopes: Array<{ __typename?: 'Scope', name: string, description: string } | null> } }; + +export type ServerInfoBlobSizeLimitQueryVariables = Exact<{ [key: string]: never; }>; + + +export type ServerInfoBlobSizeLimitQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', blobSizeLimitBytes: number } }; export type StreamCommitsQueryVariables = Exact<{ id: Scalars['String']; @@ -1814,6 +1822,11 @@ export const UsersOwnInviteFields = gql` } } ${LimitedUserFields}`; +export const ServerInfoBlobSizeFields = gql` + fragment ServerInfoBlobSizeFields on ServerInfo { + blobSizeLimitBytes +} + `; export const MainServerInfoFields = gql` fragment MainServerInfoFields on ServerInfo { name @@ -2031,11 +2044,20 @@ export const FullServerInfo = gql` ...MainServerInfoFields ...ServerInfoRolesFields ...ServerInfoScopesFields + ...ServerInfoBlobSizeFields } } ${MainServerInfoFields} ${ServerInfoRolesFields} -${ServerInfoScopesFields}`; +${ServerInfoScopesFields} +${ServerInfoBlobSizeFields}`; +export const ServerInfoBlobSizeLimit = gql` + query ServerInfoBlobSizeLimit { + serverInfo { + ...ServerInfoBlobSizeFields + } +} + ${ServerInfoBlobSizeFields}`; export const StreamCommits = gql` query StreamCommits($id: String!) { stream(id: $id) { @@ -2352,6 +2374,7 @@ export const CommentFullInfoFragmentDoc = {"kind":"Document","definitions":[{"ki export const StreamCollaboratorFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StreamCollaboratorFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]} as unknown as DocumentNode; export const LimitedUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]} as unknown as DocumentNode; export const UsersOwnInviteFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UsersOwnInviteFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingStreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"streamName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}},...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const ServerInfoBlobSizeFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerInfoBlobSizeFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blobSizeLimitBytes"}}]}}]} as unknown as DocumentNode; export const MainServerInfoFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MainServerInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"adminContact"}},{"kind":"Field","name":{"kind":"Name","value":"canonicalUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsOfService"}},{"kind":"Field","name":{"kind":"Name","value":"inviteOnly"}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]} as unknown as DocumentNode; export const ServerInfoRolesFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerInfoRolesFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"resourceTarget"}}]}}]}}]} as unknown as DocumentNode; export const ServerInfoScopesFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerInfoScopesFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode; @@ -2371,7 +2394,8 @@ export const BatchInviteToStreamsDocument = {"kind":"Document","definitions":[{" export const StreamObjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamObject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"object"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalChildrenCount"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"speckleType"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}}]}}]}}]} as unknown as DocumentNode; export const StreamObjectNoDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamObjectNoData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"object"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalChildrenCount"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"speckleType"}}]}}]}}]}}]} as unknown as DocumentNode; export const MainServerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MainServerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MainServerInfoFields"}}]}}]}},...MainServerInfoFieldsFragmentDoc.definitions]} as unknown as DocumentNode; -export const FullServerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FullServerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MainServerInfoFields"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerInfoRolesFields"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerInfoScopesFields"}}]}}]}},...MainServerInfoFieldsFragmentDoc.definitions,...ServerInfoRolesFieldsFragmentDoc.definitions,...ServerInfoScopesFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const FullServerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FullServerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MainServerInfoFields"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerInfoRolesFields"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerInfoScopesFields"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerInfoBlobSizeFields"}}]}}]}},...MainServerInfoFieldsFragmentDoc.definitions,...ServerInfoRolesFieldsFragmentDoc.definitions,...ServerInfoScopesFieldsFragmentDoc.definitions,...ServerInfoBlobSizeFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const ServerInfoBlobSizeLimitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerInfoBlobSizeLimit"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerInfoBlobSizeFields"}}]}}]}},...ServerInfoBlobSizeFieldsFragmentDoc.definitions]} as unknown as DocumentNode; export const StreamCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"branchName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const StreamsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Streams"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streams"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}},{"kind":"Field","name":{"kind":"Name","value":"collaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"branchName"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"favoritedDate"}},{"kind":"Field","name":{"kind":"Name","value":"favoritesCount"}}]}}]}}]}}]} as unknown as DocumentNode; export const StreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Stream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonStreamFields"}}]}}]}},...CommonStreamFieldsFragmentDoc.definitions]} as unknown as DocumentNode; diff --git a/packages/frontend/src/graphql/server.js b/packages/frontend/src/graphql/server.js index 7212f01e8..1ce921d89 100644 --- a/packages/frontend/src/graphql/server.js +++ b/packages/frontend/src/graphql/server.js @@ -1,5 +1,11 @@ import { gql } from '@apollo/client/core' +export const serverInfoBlobSizeFragment = gql` + fragment ServerInfoBlobSizeFields on ServerInfo { + blobSizeLimitBytes + } +` + export const mainServerInfoFieldsFragment = gql` fragment MainServerInfoFields on ServerInfo { name @@ -51,10 +57,21 @@ export const fullServerInfoQuery = gql` ...MainServerInfoFields ...ServerInfoRolesFields ...ServerInfoScopesFields + ...ServerInfoBlobSizeFields } } ${mainServerInfoFieldsFragment} ${serverInfoRolesFieldsFragment} ${serverInfoScopesFieldsFragment} + ${serverInfoBlobSizeFragment} +` + +export const serverInfoBlobSizeLimitQuery = gql` + query ServerInfoBlobSizeLimit { + serverInfo { + ...ServerInfoBlobSizeFields + } + } + ${serverInfoBlobSizeFragment} ` diff --git a/packages/frontend/src/main/components/comments/CommentEditor.vue b/packages/frontend/src/main/components/comments/CommentEditor.vue index e3ab4391b..2b704ce5c 100644 --- a/packages/frontend/src/main/components/comments/CommentEditor.vue +++ b/packages/frontend/src/main/components/comments/CommentEditor.vue @@ -3,7 +3,7 @@ result.value?.serverInfo.blobSizeLimitBytes + ) + return { blobSizeLimitBytes } + }, data() { return { editorSchemaOptions: SMART_EDITOR_SCHEMA, - fileSizeLimit: 1024 * 1024 * 25, // 25MB + // fileSizeLimit: 1024 * 1024 * 25, // 25MB countLimit: 5, // if it's more than 5, just zip it up acceptValue: [ UniqueFileTypeSpecifier.AnyImage, @@ -157,8 +167,8 @@ export default Vue.extend({ }, placeholder(): string { return this.addingComment - ? 'Your comment... (press enter to send)' - : 'Reply... (press enter to send)' + ? 'Your comment... (Enter sends it)' + : 'Reply... (Enter sends it)' }, anyAttachmentsProcessing(): boolean { return this.currentFiles.some((a) => !isUploadProcessed(a)) diff --git a/packages/frontend/src/main/lib/common/file-upload/fileUploadHelper.ts b/packages/frontend/src/main/lib/common/file-upload/fileUploadHelper.ts index f5cbdb08d..950f06432 100644 --- a/packages/frontend/src/main/lib/common/file-upload/fileUploadHelper.ts +++ b/packages/frontend/src/main/lib/common/file-upload/fileUploadHelper.ts @@ -139,22 +139,28 @@ export function isFileTypeSpecifier(type: string): type is FileTypeSpecifier { * Create a human readable file size string from the numeric size in bytes */ export function prettyFileSize(sizeInBytes: number): string { + function removeTrailingZeros(fileSize: number): string { + const fileSizeString = fileSize.toFixed(2) + const parts = fileSizeString.split('.') + if (parts[1] === '00') return parts[0] + return fileSizeString + } if (sizeInBytes < 1024) { return `${sizeInBytes}bytes` } const kbSize = sizeInBytes / 1024 if (kbSize < 1024) { - return `${kbSize.toFixed(2)}kb` + return `${removeTrailingZeros(kbSize)}kB` } const mbSize = kbSize / 1024 if (mbSize < 1024) { - return `${mbSize.toFixed(2)}mb` + return `${removeTrailingZeros(mbSize)}MB` } const gbSize = mbSize / 1024 - return `${gbSize.toFixed(2)}gb` + return `${removeTrailingZeros(gbSize)}GB` } /** diff --git a/packages/frontend/src/main/pages/stream/TheUploads.vue b/packages/frontend/src/main/pages/stream/TheUploads.vue index 0ae14545e..e3830b8f1 100644 --- a/packages/frontend/src/main/pages/stream/TheUploads.vue +++ b/packages/frontend/src/main/pages/stream/TheUploads.vue @@ -97,7 +97,8 @@ Drag and drop your file here!
- Maximum 5 files at a time. Size is restricted to 50mb each. + Maximum 5 files at a time. Size is restricted to + {{ fileSizeLimit }} mb each. result.value?.serverInfo.blobSizeLimitBytes + ) + return { blobSizeLimitBytes } + }, data() { return { dragover: false, @@ -214,9 +226,10 @@ export default { return } - if (file.size > 104857600) { - this.dragError = - 'Your files are too powerful (for now). Maximum upload size is 100mb!' + if (file.size > this.blobSizeLimitBytes) { + this.dragError = `Your files are too powerful (for now). Maximum upload size is ${prettyFileSize( + this.blobSizeLimitBytes + )} mb!` return } diff --git a/packages/server/app.js b/packages/server/app.js index a711cfb30..9b149c3d0 100644 --- a/packages/server/app.js +++ b/packages/server/app.js @@ -22,7 +22,7 @@ const { buildContext } = require('./modules/shared') const knex = require('./db/knex') const { monitorActiveConnections } = require('./logging/httpServerMonitoring') const { buildErrorFormatter } = require('@/modules/core/graph/setup') -const { isDevEnv, isTestEnv } = require('@/modules/core/helpers/envHelper') +const { isDevEnv, isTestEnv } = require('@/modules/shared/helpers/envHelper') let graphqlServer diff --git a/packages/server/bootstrap.js b/packages/server/bootstrap.js index 1bddec543..de9e1b57b 100644 --- a/packages/server/bootstrap.js +++ b/packages/server/bootstrap.js @@ -14,7 +14,7 @@ const { isApolloMonitoringEnabled, getApolloServerVersion, getServerVersion -} = require('./modules/core/helpers/envHelper') +} = require('./modules/shared/helpers/envHelper') if (isApolloMonitoringEnabled() && !getApolloServerVersion()) { process.env.APOLLO_SERVER_USER_VERSION = getServerVersion() diff --git a/packages/server/modules/blobstorage/graph/resolvers/index.js b/packages/server/modules/blobstorage/graph/resolvers/index.js index 93d72a0bb..a8fefb177 100644 --- a/packages/server/modules/blobstorage/graph/resolvers/index.js +++ b/packages/server/modules/blobstorage/graph/resolvers/index.js @@ -1,12 +1,18 @@ const { getBlobMetadata, getBlobMetadataCollection, - blobCollectionSummary + blobCollectionSummary, + getFileSizeLimit } = require('@/modules/blobstorage/services') const { NotFoundError, ResourceMismatch } = require('@/modules/shared/errors') const { UserInputError } = require('apollo-server-errors') module.exports = { + ServerInfo: { + blobSizeLimitBytes() { + return getFileSizeLimit() + } + }, Stream: { async blobs(parent, args) { const streamId = parent.id diff --git a/packages/server/modules/blobstorage/graph/schemas/blobstorage.graphql b/packages/server/modules/blobstorage/graph/schemas/blobstorage.graphql index 478b9a916..09e5f8eea 100644 --- a/packages/server/modules/blobstorage/graph/schemas/blobstorage.graphql +++ b/packages/server/modules/blobstorage/graph/schemas/blobstorage.graphql @@ -1,3 +1,7 @@ +extend type ServerInfo { + blobSizeLimitBytes: Int! +} + extend type Stream { """ Get the metadata collection of blobs stored for this stream. diff --git a/packages/server/modules/blobstorage/index.js b/packages/server/modules/blobstorage/index.js index 31fe58e9a..d7f15990f 100644 --- a/packages/server/modules/blobstorage/index.js +++ b/packages/server/modules/blobstorage/index.js @@ -25,7 +25,8 @@ const { markUploadOverFileSizeLimit, deleteBlob, getBlobMetadata, - getBlobMetadataCollection + getBlobMetadataCollection, + getFileSizeLimit } = require('@/modules/blobstorage/services') const { NotFoundError, @@ -82,8 +83,7 @@ exports.init = async (app) => { const finalizePromises = [] const busboy = Busboy({ headers: req.headers, - // this is 100 MB which matches the current frontend file size limit - limits: { fileSize: 104_857_600 } + limits: { fileSize: getFileSizeLimit() } }) const streamId = req.params.streamId busboy.on('file', (formKey, file, info) => { diff --git a/packages/server/modules/blobstorage/services.js b/packages/server/modules/blobstorage/services.js index 62f82c058..66b78410c 100644 --- a/packages/server/modules/blobstorage/services.js +++ b/packages/server/modules/blobstorage/services.js @@ -4,6 +4,7 @@ const { ResourceMismatch, BadRequestError } = require('@/modules/shared/errors') +const { getFileSizeLimitMB } = require('@/modules/shared/helpers/envHelper') const BlobStorage = () => knex('blob_storage') const blobLookup = ({ blobId, streamId }) => @@ -154,6 +155,8 @@ const updateBlobMetadata = async (streamId, blobId, updateCallback) => { return { blobId, fileName, ...updateData } } +const getFileSizeLimit = () => getFileSizeLimitMB() * 1024 * 1024 + module.exports = { cursorFromRows, decodeCursor, @@ -167,5 +170,6 @@ module.exports = { getBlobMetadataCollection, blobCollectionSummary, getBlobs, - getBlob + getBlob, + getFileSizeLimit } diff --git a/packages/server/modules/core/helpers/envHelper.js b/packages/server/modules/shared/helpers/envHelper.js similarity index 72% rename from packages/server/modules/core/helpers/envHelper.js rename to packages/server/modules/shared/helpers/envHelper.js index 39f4c0f60..a2aa1615d 100644 --- a/packages/server/modules/core/helpers/envHelper.js +++ b/packages/server/modules/shared/helpers/envHelper.js @@ -22,11 +22,19 @@ function getApolloServerVersion() { return process.env.APOLLO_SERVER_USER_VERSION } +function getFileSizeLimitMB() { + let sizeLimit = 100 + const suppliedSize = parseInt(process.env.FILE_SIZE_LIMIT_MB) + if (suppliedSize) sizeLimit = suppliedSize + return sizeLimit +} + module.exports = { isTestEnv, isDevEnv, isProdEnv, getServerVersion, isApolloMonitoringEnabled, - getApolloServerVersion + getApolloServerVersion, + getFileSizeLimitMB } diff --git a/utils/1click_image_scripts/template-docker-compose.yml b/utils/1click_image_scripts/template-docker-compose.yml index f0a90e1a5..c6737fc36 100644 --- a/utils/1click_image_scripts/template-docker-compose.yml +++ b/utils/1click_image_scripts/template-docker-compose.yml @@ -41,6 +41,8 @@ services: restart: always ports: - '127.0.0.1:8000:80' + environment: + FILE_SIZE_LIMIT_MB: 100 speckle-server: image: speckle/speckle-server:2 @@ -78,6 +80,8 @@ services: S3_BUCKET: 'speckle-server' S3_CREATE_BUCKET: 'true' + FILE_SIZE_LIMIT_MB: 100 + speckle-preview-service: image: speckle/speckle-preview-service:2 restart: always diff --git a/utils/helm/speckle-server/templates/deployment-backend.yml b/utils/helm/speckle-server/templates/deployment-backend.yml index 21e879727..f9a95e8d9 100644 --- a/utils/helm/speckle-server/templates/deployment-backend.yml +++ b/utils/helm/speckle-server/templates/deployment-backend.yml @@ -94,6 +94,9 @@ spec: name: "{{ .Values.secretName }}" key: session_secret + - name: FILE_SIZE_LIMIT_MB + value: {{ .Values.file_size_limit_mb | quote }} + # *** Redis *** - name: REDIS_URL valueFrom: diff --git a/utils/helm/speckle-server/templates/deployment-fileimport-service.yml b/utils/helm/speckle-server/templates/deployment-fileimport-service.yml index b3c313d9d..f5a70b9b3 100644 --- a/utils/helm/speckle-server/templates/deployment-fileimport-service.yml +++ b/utils/helm/speckle-server/templates/deployment-fileimport-service.yml @@ -90,5 +90,7 @@ spec: name: {{ .Values.secretName }} key: s3_secret_key + - name: FILE_IMPORT_TIME_LIMIT_MIN + value: {{ .Values.fileimport_service.time_limit_min }} {{- end }} diff --git a/utils/helm/speckle-server/templates/deployment-frontend.yml b/utils/helm/speckle-server/templates/deployment-frontend.yml index aeb15db5e..6e23ff630 100644 --- a/utils/helm/speckle-server/templates/deployment-frontend.yml +++ b/utils/helm/speckle-server/templates/deployment-frontend.yml @@ -43,3 +43,7 @@ spec: port: 80 initialDelaySeconds: 5 periodSeconds: 5 + + env: + - name: FILE_SIZE_LIMIT_MB + value: {{ .Values.file_size_limit_mb | quote }} diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 620e024db..f8d2632df 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -102,6 +102,7 @@ fileimport_service: limits: cpu: 1000m memory: 2Gi + time_limit_min: 10 secretName: server-vars @@ -111,3 +112,4 @@ cert_manager_issuer: letsencrypt-staging helm_test_enabled: true create_namespace: false +file_size_limit_mb: 100 diff --git a/utils/helm/test-values.yml b/utils/helm/test-values.yml index 35d9b42b3..a9f06b013 100644 --- a/utils/helm/test-values.yml +++ b/utils/helm/test-values.yml @@ -16,3 +16,4 @@ s3: cert_manager_issuer: ~ enable_prometheus_monitoring: true +file_size_limit_mb: 300