-
Notifications
You must be signed in to change notification settings - Fork 6
Indiana #901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Indiana #901
Changes from all commits
3b2feb0
5c4cb6b
4d8ddff
1607d75
aad283d
6ee4958
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Include any files or directories that you don't want to be copied to your | ||
| # container here (e.g., local build artifacts, temporary files, etc.). | ||
| # | ||
| # For more help, visit the .dockerignore file reference guide at | ||
| # https://docs.docker.com/go/build-context-dockerignore/ | ||
|
|
||
| **/.DS_Store | ||
| **/__pycache__ | ||
| **/.venv | ||
| **/.classpath | ||
| **/.dockerignore | ||
| **/.env | ||
| **/.git | ||
| **/.gitignore | ||
| **/.project | ||
| **/.settings | ||
| **/.toolstarget | ||
| **/.vs | ||
| **/.vscode | ||
| **/*.*proj.user | ||
| **/*.dbmdl | ||
| **/*.jfm | ||
| **/bin | ||
| **/charts | ||
| **/docker-compose* | ||
| **/compose.y*ml | ||
| **/Dockerfile* | ||
| **/node_modules | ||
| **/npm-debug.log | ||
| **/obj | ||
| **/secrets.dev.yaml | ||
| **/values.dev.yaml | ||
| LICENSE | ||
| README.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,6 @@ | |
|
|
||
| e2e/npm-debug.log | ||
|
|
||
| **/.tmp/** | ||
| **/.temp/** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # syntax=docker/dockerfile:1 | ||
|
|
||
| # Comments are provided throughout this file to help you get started. | ||
| # If you need more help, visit the Dockerfile reference guide at | ||
| # https://docs.docker.com/go/dockerfile-reference/ | ||
|
|
||
| # Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 | ||
|
|
||
| ################################################################################ | ||
| # Pick a base image to serve as the foundation for the other build stages in | ||
| # this file. | ||
| # | ||
| # For illustrative purposes, the following FROM command | ||
| # is using the alpine image (see https://hub.docker.com/_/alpine). | ||
| # By specifying the "latest" tag, it will also use whatever happens to be the | ||
| # most recent version of that image when you build your Dockerfile. | ||
| # If reproducibility is important, consider using a versioned tag | ||
| # (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). | ||
| FROM alpine:latest as base | ||
|
|
||
| ################################################################################ | ||
| # Create a stage for building/compiling the application. | ||
| # | ||
| # The following commands will leverage the "base" stage above to generate | ||
| # a "hello world" script and make it executable, but for a real application, you | ||
| # would issue a RUN command for your application's build process to generate the | ||
| # executable. For language-specific examples, take a look at the Dockerfiles in | ||
| # the Awesome Compose repository: https://github.com/docker/awesome-compose | ||
| FROM base as build | ||
| RUN echo -e '#!/bin/sh\n\ | ||
| echo Hello world from $(whoami)! In order to get your application running in a container, take a look at the comments in the Dockerfile to get started.'\ | ||
| > /bin/hello.sh | ||
| RUN chmod +x /bin/hello.sh | ||
|
|
||
| ################################################################################ | ||
| # Create a final stage for running your application. | ||
| # | ||
| # The following commands copy the output from the "build" stage above and tell | ||
| # the container runtime to execute it when the image is run. Ideally this stage | ||
| # contains the minimal runtime dependencies for the application as to produce | ||
| # the smallest image possible. This often means using a different and smaller | ||
| # image than the one used for building the application, but for illustrative | ||
| # purposes the "base" image is used here. | ||
| FROM base AS final | ||
|
|
||
| # Create a non-privileged user that the app will run under. | ||
| # See https://docs.docker.com/go/dockerfile-user-best-practices/ | ||
| ARG UID=10001 | ||
| RUN adduser \ | ||
| --disabled-password \ | ||
| --gecos "" \ | ||
| --home "/nonexistent" \ | ||
| --shell "/sbin/nologin" \ | ||
| --no-create-home \ | ||
| --uid "${UID}" \ | ||
| appuser | ||
| USER appuser | ||
|
|
||
| # Copy the executable from the "build" stage. | ||
| COPY --from=build /bin/hello.sh /bin/ | ||
|
|
||
| # What the container should run when it is started. | ||
| ENTRYPOINT [ "/bin/hello.sh" ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| ### Building and running your application | ||
|
|
||
| When you're ready, start your application by running: | ||
| `docker compose up --build`. | ||
|
|
||
| ### Deploying your application to the cloud | ||
|
|
||
| First, build your image, e.g.: `docker build -t myapp .`. | ||
| If your cloud uses a different CPU architecture than your development | ||
| machine (e.g., you are on a Mac M1 and your cloud provider is amd64), | ||
| you'll want to build the image for that platform, e.g.: | ||
| `docker build --platform=linux/amd64 -t myapp .`. | ||
|
|
||
| Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. | ||
|
|
||
| Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) | ||
| docs for more detail on building and pushing. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Comments are provided throughout this file to help you get started. | ||
| # If you need more help, visit the Docker Compose reference guide at | ||
| # https://docs.docker.com/go/compose-spec-reference/ | ||
|
|
||
| # Here the instructions define your application as a service called "app". | ||
| # This service is built from the Dockerfile in the current directory. | ||
| # You can add other services your application may depend on here, such as a | ||
| # database or a cache. For examples, see the Awesome Compose repository: | ||
| # https://github.com/docker/awesome-compose | ||
| services: | ||
| app: | ||
| build: | ||
| context: . | ||
| target: final | ||
| # If your application exposes a port, uncomment the following lines and change | ||
| # the port numbers as needed. The first number is the host port and the second | ||
| # is the port inside the container. | ||
| # ports: | ||
| # - 8080:8080 | ||
|
|
||
| # The commented out section below is an example of how to define a PostgreSQL | ||
| # database that your application can use. `depends_on` tells Docker Compose to | ||
| # start the database before your application. The `db-data` volume persists the | ||
| # database data between container restarts. The `db-password` secret is used | ||
| # to set the database password. You must create `db/password.txt` and add | ||
| # a password of your choosing to it before running `docker compose up`. | ||
| # depends_on: | ||
| # db: | ||
| # condition: service_healthy | ||
| # db: | ||
| # image: postgres | ||
| # restart: always | ||
| # user: postgres | ||
| # secrets: | ||
| # - db-password | ||
| # volumes: | ||
| # - db-data:/var/lib/postgresql/data | ||
| # environment: | ||
| # - POSTGRES_DB=example | ||
| # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password | ||
| # expose: | ||
| # - 5432 | ||
| # healthcheck: | ||
| # test: [ "CMD", "pg_isready" ] | ||
| # interval: 10s | ||
| # timeout: 5s | ||
| # retries: 5 | ||
| # volumes: | ||
| # db-data: | ||
| # secrets: | ||
| # db-password: | ||
| # file: db/password.txt |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
IndianaEunice marked this conversation as resolved.
|
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exclude this file from PR merge request. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need clarification, are images stored in the database or in the azure blob? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { | ||
| Controller, | ||
| Post, | ||
| Get, | ||
| Param, | ||
| Res, | ||
| UploadedFile, | ||
| UseInterceptors, | ||
| BadRequestException, | ||
| NotFoundException, | ||
| } from '@nestjs/common'; | ||
| import { FileInterceptor } from '@nestjs/platform-express'; | ||
| import { Response } from 'express'; | ||
| import { join } from 'path'; | ||
| import * as fs from 'fs'; | ||
| import { AzureBlobService } from '../azure-blob/azure-blob.service'; | ||
| import { SpeciesInformationService } from './speciesInformation.service'; | ||
|
|
||
| // CHANGED: TypeScript kept resolving sharp's type declarations as | ||
| // non-callable in this project's config, regardless of import style | ||
| // (`import sharp from`, `import * as sharp from`, and | ||
| // `import sharp = require(...)` all failed the same way). Plain | ||
| // require() sidesteps that entirely — sharp becomes typed as `any` | ||
| // and isn't type-checked at all, only used at runtime. | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const sharp = require('sharp'); | ||
|
|
||
| @Controller('species-information') | ||
| export class SpeciesInformationController { | ||
| constructor( | ||
| private readonly azureBlobService: AzureBlobService, | ||
| private readonly speciesInformationService: SpeciesInformationService, | ||
| ) {} | ||
|
|
||
| // Uploads a new species image. Two things happen here: | ||
| // 1. The original JPEG is uploaded as-is (this becomes speciesImage). | ||
| // 2. A smaller WebP copy is generated on the server and uploaded too | ||
| // (this becomes previewImage). The frontend never has to do any | ||
| // image conversion itself. | ||
| @Post('upload-image') | ||
| @UseInterceptors(FileInterceptor('file')) | ||
| async uploadImage(@UploadedFile() file: Express.Multer.File) { | ||
| if (!file) { | ||
| throw new BadRequestException('No file provided'); | ||
| } | ||
|
|
||
| // Only JPEG is accepted, since that's the documented format and | ||
| // the only one we're set up to convert to WebP here. | ||
| if (file.mimetype !== 'image/jpeg') { | ||
| throw new BadRequestException('Only JPEG images are supported'); | ||
| } | ||
|
|
||
| // Step 1: upload the original JPEG, unchanged | ||
| const originalResult = await this.azureBlobService.upload( | ||
| file, | ||
| 'species-images', | ||
| ); | ||
|
|
||
| // Step 2: build a smaller WebP version in memory. | ||
| // - resize() caps the width so the preview is genuinely lighter | ||
| // - withoutEnlargement stops small images being blown up | ||
| // - webp({ quality: 80 }) is a solid size/quality balance | ||
| const previewBuffer = await sharp(file.buffer) | ||
| .resize({ width: 800, withoutEnlargement: true }) | ||
| .webp({ quality: 80 }) | ||
| .toBuffer(); | ||
|
|
||
| // Step 3: upload that WebP buffer as its own file, in its own | ||
| // container, so it's a completely separate object from the original | ||
| const previewFile: Express.Multer.File = { | ||
| ...file, | ||
| buffer: previewBuffer, | ||
| size: previewBuffer.length, | ||
| mimetype: 'image/webp', | ||
| originalname: file.originalname.replace(/\.jpe?g$/i, '.webp'), | ||
| }; | ||
| const previewResult = await this.azureBlobService.upload( | ||
| previewFile, | ||
| 'species-images-preview', | ||
| ); | ||
|
|
||
| // Store only the bare filename (no directory prefix, no host/URL) so | ||
| // it matches the format the rest of the species_information table | ||
| // uses, and what the /images/:filename route expects when it looks | ||
| // the file up under public/species-images/. | ||
| const stripDirectory = (filePath: string) => filePath.split('/').pop(); | ||
|
|
||
| return { | ||
| imageUrl: stripDirectory(originalResult.filePath), | ||
| fileName: stripDirectory(originalResult.filePath), | ||
| previewImageUrl: stripDirectory(previewResult.filePath), | ||
| previewFileName: stripDirectory(previewResult.filePath), | ||
| }; | ||
| } | ||
|
|
||
| // On-demand download route. The list page never loads speciesImage, | ||
| // so when someone clicks "Download" there, the frontend calls this | ||
| // route with just the species id. We look up speciesImage here and | ||
| // redirect the browser to the real file — either straight to Azure | ||
| // (if it's already a full URL) or through our own local image route | ||
| // (if it's just a bare filename sitting in the flat species-images | ||
| // folder, as with the manually-populated legacy rows). | ||
| @Get(':id/download-image') | ||
| async downloadImage(@Param('id') id: string, @Res() res: Response) { | ||
| const species = | ||
| await this.speciesInformationService.getSpeciesImageForDownload(id); | ||
|
|
||
| if (!species || !species.speciesImage) { | ||
| throw new NotFoundException('Image not found'); | ||
| } | ||
|
|
||
| const isFullUrl = | ||
| species.speciesImage.startsWith('http://') || | ||
| species.speciesImage.startsWith('https://'); | ||
|
|
||
| const redirectTarget = isFullUrl | ||
| ? species.speciesImage | ||
| : `/vector-api/species-information/images/${species.speciesImage}`; | ||
|
|
||
| return res.redirect(redirectTarget); | ||
| } | ||
|
|
||
| @Get('images/:filename') | ||
| async getImage(@Param('filename') filename: string, @Res() res: Response) { | ||
| const filePath = join(process.cwd(), 'public', 'species-images', filename); | ||
|
|
||
| if (!fs.existsSync(filePath)) { | ||
| throw new NotFoundException('Image not found'); | ||
| } | ||
| return res.sendFile(filePath); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exclude this file from PR merge request.