@jnode/gemini
Version:
Simple Gemini API package for Node.js.
198 lines (164 loc) • 6.53 kB
JavaScript
/*
JustGemini/file.js
v.1.0.0
Simple Gemini API package for Node.js.
by JustNode Dev Team / JustApple
*/
//load nodejs packages
const http = require('http');
const https = require('https');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
//load jnode packages
const request = require('@jnode/request');
//load supported mime type
const supportMimeTypes = require('./mime.json');
const mimeTypesArray = Object.values(supportMimeTypes);
//Gemini file manager
class GeminiFileManager {
constructor(client) {
this.client = client;
this.filesCache = [];
}
//upload local file or web file to gemini api with smart handling
uploadFile(file, isWebFile = false, displayName, keyOverwrite) {
return new Promise(async (resolve, reject) => {
let fileSize; //file bytes
let fileType = 'text/plain'; //file mime type
let fileMtime = 0; //file modify time
let fileStream; //file read stream
//get file info and read stream
if (isWebFile) { //handle files from internet (http/https)
const web = file.startsWith('https://') ? https : http; //select from http or https
await new Promise((resolve, reject) => {
web.get(file, (res) => { //fetch file
if (res.statusCode > 299 || res.statusCode < 200) { //check http error
let err = new Error(`Web file responded with code ${res.statusCode}.`);
err.headers = res.headers; //provide headers
res.destroy(); //destory connect
reject(err); //throw error
return;
};
//check unsupport file type and throw error
if (!mimeTypesArray.includes(res.headers['content-type'] ?? 'text/plain')) {
res.destroy(); //destory connect
reject(new Error(`Unsupported file MIME type: ${res.headers['content-type']}.`));
return;
}
//set file info and read stream
fileType = res.headers['content-type'] ?? 'text/plain';
fileSize = res.headers['content-length'] ?? null;
fileMtime = new Date(res.headers['last-modified'] ?? 0).getTime();
fileStream = res;
resolve();
});
});
} else { //handle local file
file = path.resolve(file); //resolve path
let fileStat;
try {
fileStat = await fs.promises.stat(file); //get file info
} catch (err) {
reject(new Error(`Fail to read file "${file}".`)); //could not get file
}
//check unsupport file type and throw error
if (!mimeTypesArray.includes(supportMimeTypes[path.extname(file)] ?? 'text/plain')) {
reject(new Error(`Unsupported file MIME type: ${supportMimeTypes[path.extname(file)]}.`));
return;
}
//set file info and read stream
fileType = supportMimeTypes[path.extname(file)] ?? 'text/plain';
fileSize = fileStat.size ?? null;
fileMtime = fileStat.mtimeMs ?? 0;
fileStream = fs.createReadStream(file);
}
//set smart display name
displayName = displayName ?? this.getSmartName(file, isWebFile, fileMtime);
//make a multipart request
const res = await request.multipartRequest('POST', this.client.apiUrl('/upload/v1beta/files', null, keyOverwrite), {
'X-Goog-Upload-Protocol': 'multipart',
'X-Goog-Upload-Header-Content-Length': fileSize,
'X-Goog-Upload-Header-Content-Type': fileType
}, [
{
type: 'application/json; charset=utf-8',
data: JSON.stringify({ file: { displayName: displayName} })
},
{
type: fileType,
stream: fileStream
}
]);
if (res.statusCode !== 200) { //api responded with error
let err = new Error(`Gemini API responded with code ${res.statusCode}.`);
err.headers = res.headers; //provide headers
err.body = res.body; //provide body
reject(err);
return;
}
let result = res.json().file;
for (let i = 0; (i < this.client.fileMaxActiveCheck && result.state !== 'ACTIVE'); i++) { //check file status until is active
result = (await this.client.apiRequest('GET', `/v1beta/${result.name}`, undefined, undefined, keyOverwrite)).json();
await new Promise((resolve) => {
setTimeout(resolve, this.client.fileActiveCheckDelay);
});
}
resolve(result); //return result
});
}
async getFilesList(keyOverwrite) {
this.filesCache = (await this.client.apiRequest('GET', '/v1beta/files', 'pageSize=100', undefined, keyOverwrite)).json().files ?? [];
return this.filesCache;
}
apiDeleteFile(file, keyOverwrite) {
return this.client.apiRequest('DELETE', `/v1beta/${file}`, undefined, undefined, keyOverwrite);
}
async getSmartFile(file, isWebFile = false, keyOverwrite) {
let fileMtime = 0; //file modify time
if (isWebFile) { //handle files from internet (http/https)
const web = file.startsWith('https://') ? https : http; //select from http or https
await new Promise((resolve, reject) => {
web.get(file, (res) => { //fetch file
if (res.statusCode > 299 || res.statusCode < 200) { //check http error
let err = new Error(`Web file responded with code ${res.statusCode}.`);
err.headers = res.headers; //provide headers
res.destroy(); //destory connect
reject(err); //throw error
return;
};
//check unsupport file type and throw error
if (!mimeTypesArray.includes(res.headers['content-type'] ?? 'text/plain')) {
res.destroy(); //destory connect
reject(new Error(`Unsupported file MIME type: ${res.headers['content-type']}.`));
return;
}
res.destroy(); //destory connect
//set file info and read stream
fileMtime = new Date(res.headers['last-modified'] ?? 0).getTime();
resolve();
});
});
} else { //handle local file
file = path.resolve(file); //resolve path
let fileStat;
try {
fileStat = await fs.promises.stat(file); //get file info
} catch (err) {
throw new Error(`Fail to read file "${file}".`); //could not get file
}
//set file mtime
fileMtime = fileStat.mtimeMs ?? 0;
}
const displayName = this.getSmartName(file, isWebFile, fileMtime);
await this.getFilesList(keyOverwrite); //update files list
return this.filesCache.find((e) => e.displayName === displayName);
}
getSmartName(file, isWebFile, mtime) {
const hashSum = crypto.createHash('sha256');
hashSum.update(path.resolve(file)); //hash file path/url
//format: <HEX HASH> <IS WEB FILE> <MTIME>
return hashSum.digest('hex') + ' ' + (isWebFile ? '1' : '0') + ' ' + mtime;
}
}
module.exports = GeminiFileManager;