File size: 10,654 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const fetch = require('node-fetch');
const { logger } = require('@librechat/data-schemas');
const { getFirebaseStorage } = require('@librechat/api');
const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage');
const { getBufferMetadata } = require('~/server/utils');

/**
 * Deletes a file from Firebase Storage.
 * @param {string} directory - The directory name
 * @param {string} fileName - The name of the file to delete.
 * @returns {Promise<void>} A promise that resolves when the file is deleted.
 */
async function deleteFile(basePath, fileName) {
  const storage = getFirebaseStorage();
  if (!storage) {
    logger.error('Firebase is not initialized. Cannot delete file from Firebase Storage.');
    throw new Error('Firebase is not initialized');
  }

  const storageRef = ref(storage, `${basePath}/${fileName}`);

  try {
    await deleteObject(storageRef);
    logger.debug('File deleted successfully from Firebase Storage');
  } catch (error) {
    logger.error('Error deleting file from Firebase Storage:', error.message);
    throw error;
  }
}

/**
 * Saves an file from a given URL to Firebase Storage. The function first initializes the Firebase Storage
 * reference, then uploads the file to a specified basePath in the Firebase Storage. It handles initialization
 * errors and upload errors, logging them to the console. If the upload is successful, the file name is returned.
 *
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The user's unique identifier. This is used to create a user-specific basePath
 *                                 in Firebase Storage.
 * @param {string} params.URL - The URL of the file to be uploaded. The file at this URL will be fetched
 *                              and uploaded to Firebase Storage.
 * @param {string} params.fileName - The name that will be used to save the file in Firebase Storage. This
 *                                   should include the file extension.
 * @param {string} [params.basePath='images'] - Optional. The base basePath in Firebase Storage where the file will
 *                                          be stored. Defaults to 'images' if not specified.
 *
 * @returns {Promise<{ bytes: number, type: string, dimensions: Record<string, number>} | null>}
 *          A promise that resolves to the file metadata if the file is successfully saved, or null if there is an error.
 */
async function saveURLToFirebase({ userId, URL, fileName, basePath = 'images' }) {
  const storage = getFirebaseStorage();
  if (!storage) {
    logger.error('Firebase is not initialized. Cannot save file to Firebase Storage.');
    return null;
  }

  const storageRef = ref(storage, `${basePath}/${userId.toString()}/${fileName}`);
  const response = await fetch(URL);
  const buffer = await response.buffer();

  try {
    await uploadBytes(storageRef, buffer);
    return await getBufferMetadata(buffer);
  } catch (error) {
    logger.error('Error uploading file to Firebase Storage:', error.message);
    return null;
  }
}

/**
 * Retrieves the download URL for a specified file from Firebase Storage. This function initializes the
 * Firebase Storage and generates a reference to the file based on the provided basePath and file name. If
 * Firebase Storage is not initialized or if there is an error in fetching the URL, the error is logged
 * to the console.
 *
 * @param {Object} params - The parameters object.
 * @param {string} params.fileName - The name of the file for which the URL is to be retrieved. This should
 *                                   include the file extension.
 * @param {string} [params.basePath='images'] - Optional. The base basePath in Firebase Storage where the file is
 *                                          stored. Defaults to 'images' if not specified.
 *
 * @returns {Promise<string|null>}
 *          A promise that resolves to the download URL of the file if successful, or null if there is an
 *          error in initialization or fetching the URL.
 */
async function getFirebaseURL({ fileName, basePath = 'images' }) {
  const storage = getFirebaseStorage();
  if (!storage) {
    logger.error('Firebase is not initialized. Cannot get image URL from Firebase Storage.');
    return null;
  }

  const storageRef = ref(storage, `${basePath}/${fileName}`);

  try {
    return await getDownloadURL(storageRef);
  } catch (error) {
    logger.error('Error fetching file URL from Firebase Storage:', error.message);
    return null;
  }
}

/**
 * Uploads a buffer to Firebase Storage.
 *
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The user's unique identifier. This is used to create a user-specific basePath
 *                                 in Firebase Storage.
 * @param {string} params.fileName - The name of the file to be saved in Firebase Storage.
 * @param {string} params.buffer - The buffer to be uploaded.
 * @param {string} [params.basePath='images'] - Optional. The base basePath in Firebase Storage where the file will
 *                                          be stored. Defaults to 'images' if not specified.
 *
 * @returns {Promise<string>} - A promise that resolves to the download URL of the uploaded file.
 */
async function saveBufferToFirebase({ userId, buffer, fileName, basePath = 'images' }) {
  const storage = getFirebaseStorage();
  if (!storage) {
    throw new Error('Firebase is not initialized');
  }

  const storageRef = ref(storage, `${basePath}/${userId}/${fileName}`);
  await uploadBytes(storageRef, buffer);

  // Assuming you have a function to get the download URL
  return await getFirebaseURL({ fileName, basePath: `${basePath}/${userId}` });
}

/**
 * Extracts and decodes the file path from a Firebase Storage URL.
 *
 * @param {string} urlString - The Firebase Storage URL.
 * @returns {string} The decoded file path.
 */
function extractFirebaseFilePath(urlString) {
  try {
    const url = new URL(urlString);
    const pathRegex = /\/o\/(.+?)(\?|$)/;
    const match = url.pathname.match(pathRegex);

    if (match && match[1]) {
      return decodeURIComponent(match[1]);
    }

    return '';
  } catch {
    logger.debug(
      '[extractFirebaseFilePath] Failed to extract Firebase file path from URL, returning empty string',
    );
    // If URL parsing fails, return an empty string
    return '';
  }
}

/**
 * Deletes a file from Firebase storage. This function determines the filepath from the
 * Firebase storage URL via regex for deletion. Validated by the user's ID.
 *
 * @param {ServerRequest} req - The request object from Express.
 * It should contain a `user` object with an `id` property.
 * @param {MongoFile} file - The file object to be deleted.
 *
 * @returns {Promise<void>}
 *          A promise that resolves when the file has been successfully deleted from Firebase storage.
 *          Throws an error if there is an issue with deletion.
 */
const deleteFirebaseFile = async (req, file) => {
  if (file.embedded && process.env.RAG_API_URL) {
    const jwtToken = req.headers.authorization.split(' ')[1];
    try {
      await axios.delete(`${process.env.RAG_API_URL}/documents`, {
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          'Content-Type': 'application/json',
          accept: 'application/json',
        },
        data: [file.file_id],
      });
    } catch (error) {
      if (error.response?.status === 404) {
        logger.warn(
          `[deleteFirebaseFile] Document ${file.file_id} not found in RAG API, may have been deleted already`,
        );
      } else {
        logger.error('[deleteFirebaseFile] Error deleting document from RAG API:', error);
      }
    }
  }

  const fileName = extractFirebaseFilePath(file.filepath);
  if (!fileName.includes(req.user.id)) {
    throw new Error('Invalid file path');
  }
  try {
    await deleteFile('', fileName);
  } catch (error) {
    logger.error('Error deleting file from Firebase:', error);
    if (error.code === 'storage/object-not-found') {
      return;
    }
    throw error;
  }
};

/**
 * Uploads a file to Firebase Storage.
 *
 * @param {Object} params - The params object.
 * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
 *                       representing the user.
 * @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
 *                                     have a `path` property that points to the location of the uploaded file.
 * @param {string} params.file_id - The file ID.
 *
 * @returns {Promise<{ filepath: string, bytes: number }>}
 *          A promise that resolves to an object containing:
 *            - filepath: The download URL of the uploaded file.
 *            - bytes: The size of the uploaded file in bytes.
 */
async function uploadFileToFirebase({ req, file, file_id }) {
  const inputFilePath = file.path;
  const inputBuffer = await fs.promises.readFile(inputFilePath);
  const bytes = Buffer.byteLength(inputBuffer);
  const userId = req.user.id;
  const fileName = `${file_id}__${path.basename(inputFilePath)}`;
  try {
    const downloadURL = await saveBufferToFirebase({ userId, buffer: inputBuffer, fileName });
    return { filepath: downloadURL, bytes };
  } catch (err) {
    logger.error('[uploadFileToFirebase] Error saving file buffer to Firebase:', err);
    try {
      if (file && file.path) {
        await fs.promises.unlink(file.path);
      }
    } catch (unlinkError) {
      logger.error(
        '[uploadFileToFirebase] Error deleting temporary file, likely already deleted:',
        unlinkError.message,
      );
    }
    throw err;
  }
}

/**
 * Retrieves a readable stream for a file from Firebase storage.
 *
 * @param {ServerRequest} _req
 * @param {string} filepath - The filepath.
 * @returns {Promise<ReadableStream>} A readable stream of the file.
 */
async function getFirebaseFileStream(_req, filepath) {
  try {
    const storage = getFirebaseStorage();
    if (!storage) {
      throw new Error('Firebase is not initialized');
    }

    const response = await axios({
      method: 'get',
      url: filepath,
      responseType: 'stream',
    });

    return response.data;
  } catch (error) {
    logger.error('Error getting Firebase file stream:', error);
    throw error;
  }
}

module.exports = {
  deleteFile,
  getFirebaseURL,
  saveURLToFirebase,
  deleteFirebaseFile,
  uploadFileToFirebase,
  saveBufferToFirebase,
  getFirebaseFileStream,
};