File size: 2,417 Bytes
68d7816
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import type { Readable } from 'node:stream';

import { z } from 'zod';

import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes';
import { Error2 } from '#/_base/errors/errors';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';

export const fileMetaSchema = z.object({
  id: z.string().min(1),
  name: z.string().min(1),
  media_type: z.string().min(1),
  size: z.number().int().nonnegative(),
  created_at: isoDateTimeSchema,
  expires_at: isoDateTimeSchema.optional(),
});
export type FileMeta = z.infer<typeof fileMetaSchema>;

export interface SaveOptions {
  readonly name?: string;
  readonly mimeType?: string;
  readonly expiresInSec?: number;
}

export interface GetResult {
  readonly meta: FileMeta;
  readonly stream: (range?: FileReadRange) => Readable;
}

export interface FileReadRange {
  readonly start: number;
  readonly end: number;
}

export interface IFileService {
  readonly _serviceBrand: undefined;

  save(source: Readable, filename: string, options?: SaveOptions): Promise<FileMeta>;
  get(fileId: string): Promise<GetResult>;
  delete(fileId: string): Promise<void>;
}

export const IFileService: ServiceIdentifier<IFileService> = createDecorator<IFileService>('fileService');

export const FILE_ID_REGEX = /^f_[A-Za-z0-9][A-Za-z0-9_-]*$/;

export function isFileId(value: string): boolean {
  return FILE_ID_REGEX.test(value);
}

export const FileErrors = {
  codes: {
    FILE_NOT_FOUND: 'file.not_found',
  },
  info: {
    'file.not_found': {
      title: 'File not found',
      retryable: false,
      public: true,
      action: 'Check the file_id or upload the file again.',
    },
  },
} as const satisfies ErrorDomain;

registerErrorDomain(FileErrors);

export class FileError extends Error2 {
  constructor(
    code: (typeof FileErrors.codes)[keyof typeof FileErrors.codes],
    message: string,
    details?: Record<string, unknown>,
  ) {
    super(code, message, { details });
    this.name = 'FileError';
  }
}

export function fileNotFoundError(fileId: string): FileError {
  return new FileError(FileErrors.codes.FILE_NOT_FOUND, `file not found: ${fileId}`, { fileId });
}

export function isFileError(error: unknown, code: (typeof FileErrors.codes)[keyof typeof FileErrors.codes]): boolean {
  return error instanceof Error2 && error.code === code;
}