Skip to content
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

One working example showing a controller serving an image from a stream or buffer #1686

Closed
2 of 4 tasks
ManfredLange opened this issue Sep 22, 2024 · 1 comment
Closed
2 of 4 tasks

Comments

@ManfredLange
Copy link

Sorting

  • I'm submitting a ...

    • bug report
    • feature request
    • support request
  • I confirm that I

    • used the search to make sure that a similar issue hasn't already been submit

I've also used standard internet search, Github Copilot and Perplexity.ai. I also consulted the documentation.

Expected Behavior

Current Behavior

Possible Solution

Steps to Reproduce

Context (Environment)

Version of the library: 6.4.0 (@tsoa/client and @tsoa/runtime)
Version of NodeJS: 20.17.0

  • Confirm you were using yarn not npm: [ ]

I'm using pnpm but I don't think that has any material relevance.

Detailed Description

I'm trying to find one single and complete example for the following:

  1. File is stored locally in the file system
  2. Controller has a method/function that handles a GET request to get that file.
  3. The method/function returns the buffer/file and sets headers appropriately

Here is an implementation that does most of it but returns it to the browser as a json object, not the image itself:

   @Get('{imageId}')
   @Response<Buffer>('200', 'image/png')
   public async getImageById(imageId: string): Promise<Buffer | undefined> {
      const imageDirectory = this._environment.blogPostsDirectory;
      const imagePath = join(imageDirectory, imageId);

      try {
         console.log(`loc 240923-0745: reading image from ${imagePath}`);
         const image = await fs.promises.readFile(imagePath);
         this.setHeader('Content-Type', 'image/png');
         this.setHeader('Content-Length', image.length.toString());
         return image;
      } catch (error) {
         console.log(`loc 240923-0746: image ${imagePath} not found`);
         this.setStatus(404);
         return undefined;
      }
   }

It appears to me that I am overlooking something completely obvious but I haven't been able to figure it out just yet.

Perhaps someone who has more experience with TSOA than me can point me in the right direction. To me it appears that downloading a file is something quite common, so I am hoping that TSOA offers some adquate solution for this.

Thank you!

Breaking change?

@ManfredLange
Copy link
Author

ManfredLange commented Sep 22, 2024

Since this was a blocker for me, I continued my search. This time I used the ChatGPT model "o1-preview" for a conversation. After some suggestions with compile errors, I managed to get the following code out of it which appears to work as desired. I'm sharing this here, in case someone else has a similar issue. Note that this is for TSOA 6.4.0.

import { Controller, Get, Route, Response, Path, SuccessResponse } from '@tsoa/runtime';
import { inject } from 'inversify';
import { Types } from '../config/ioc.types';
import { IEnvironment } from '../config/IEnvironment';
import path, { join } from 'path';
import { createReadStream } from 'fs';
import { stat } from 'fs/promises';
import { Readable } from 'stream';
import { provide } from 'inversify-binding-decorators';

@Route('/2024-09-06/images')
@provide(ImageController)
export class ImageController extends Controller {
   public constructor(
      @inject(Types.IEnvironment) environment: IEnvironment,
   ) {
      super();
      this._environment = environment;
   }

   @Get('{imageId}')
   @SuccessResponse('200', 'OK')
   @Response(404, 'Image not found')
   public async getImage(
      @Path() imageId: string,
   ): Promise<Readable> {
      // Securely construct the full path to the image
      const imageDirectory = this._environment.blogPostsDirectory;
      const imagePath = join(imageDirectory, imageId);

      // Prevent directory traversal attacks
      if (!imagePath.startsWith(imageDirectory)) {
         this.setStatus(400);
         throw new Error('Invalid image path');
      }

      // Check if the file exists
      try {
         await stat(imagePath);
      } catch (err) {
         // File does not exist
         this.setStatus(404);
         throw new Error('Image not found');
      }

      // Determine the Content-Type based on the file extension
      const fileExtension = path.extname(imagePath).toLowerCase();
      let contentType = 'image/png'; // Default Content-Type

      switch (fileExtension) {
         case '.jpg':
         case '.jpeg':
            contentType = 'image/jpeg';
            break;
         case '.gif':
            contentType = 'image/gif';
            break;
         case '.bmp':
            contentType = 'image/bmp';
            break;
         case '.svg':
            contentType = 'image/svg+xml';
            break;
         case '.webp':
            contentType = 'image/webp';
            break;
         case '.png':
         default:
            contentType = 'image/png';
            break;
      }

      // Set the Content-Type header
      this.setHeader('Content-Type', contentType);

      // Create a read stream and return it
      return createReadStream(imagePath);
   }
   private _environment: IEnvironment;
}

Note that this code was generated by AI. I merely edited it to some degree to fit into our environment. Happy for you to use it "as-is" but keep in mind that you are responsible and accountable for any bugs that may be left in this code snippet. Happy coding!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant