data2 / index.js
blaze-aura69's picture
Update index.js
344f2e7 verified
Raw
History Blame Contribute Delete
20.1 kB
'use strict';
const { Worker: ThreadWorker, isMainThread, parentPort, workerData } = require('worker_threads');
// ============ DATABASE WORKER THREAD ============
if (!isMainThread) {
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const DATA_DIR = workerData.dataDir || "/data";
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
const dbPath = path.join(DATA_DIR, 'users.db');
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('synchronous = OFF');
db.pragma('cache_size = -2000000');
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 268435456');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
is_login INTEGER DEFAULT 0,
bio TEXT DEFAULT '',
pic_url TEXT DEFAULT '',
date_of_birth TEXT DEFAULT '',
is_profile_public TEXT DEFAULT 'true'
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_username ON users(username);`);
const stmts = {
getAllUsers: db.prepare('SELECT * FROM users'),
getUserById: db.prepare('SELECT * FROM users WHERE id = ?'),
getUserByUsername: db.prepare('SELECT * FROM users WHERE username = ?'),
insertUser: db.prepare(`
INSERT INTO users (
id, username, password_hash, name, created_at, is_login, bio, pic_url, date_of_birth, is_profile_public
) VALUES (
@id, @username, @password_hash, @name, @created_at, @is_login, @bio, @pic_url, @date_of_birth, @is_profile_public
)
`),
updateLogin: db.prepare('UPDATE users SET is_login = ? WHERE id = ?'),
updateProfile: db.prepare(`
UPDATE users SET
username = COALESCE(NULLIF(@username, ''), username),
name = COALESCE(NULLIF(@name, ''), name),
bio = COALESCE(NULLIF(@bio, ''), bio),
date_of_birth = COALESCE(NULLIF(@date_of_birth, ''), date_of_birth),
is_profile_public = COALESCE(NULLIF(@is_profile_public, ''), is_profile_public)
WHERE id = @id
`),
updatePhoto: db.prepare('UPDATE users SET pic_url = ? WHERE id = ?'),
getCount: db.prepare('SELECT COUNT(*) as count FROM users')
};
parentPort.on('message', async (message) => {
const { id, action, data } = message;
try {
let result;
switch (action) {
case 'getAllUsers':
result = stmts.getAllUsers.all();
break;
case 'getUserById':
result = stmts.getUserById.get(data.userId);
break;
case 'getUserByUsername':
result = stmts.getUserByUsername.get(data.username);
break;
case 'insertUser':
stmts.insertUser.run(data.user);
result = { success: true };
break;
case 'updateLogin':
stmts.updateLogin.run(data.isLogin ? 1 : 0, data.userId);
result = { success: true };
break;
case 'updateProfile':
stmts.updateProfile.run({
id: data.userId,
username: data.username || '',
name: data.name || '',
bio: data.bio || '',
date_of_birth: data.date_of_birth || '',
is_profile_public: data.is_profile_public || 'true'
});
result = { success: true };
break;
case 'updatePhoto':
stmts.updatePhoto.run(data.picUrl, data.userId);
result = { success: true };
break;
case 'batchOperation':
result = processBatch(data.operations);
break;
case 'getCount':
result = stmts.getCount.get().count;
break;
default:
throw new Error(`Unknown action: ${action}`);
}
parentPort.postMessage({ id, success: true, result });
} catch (error) {
parentPort.postMessage({ id, success: false, error: error.message });
}
});
function processBatch(operations) {
const transaction = db.transaction(() => {
for (const op of operations) {
switch (op.action) {
case 'signup':
try { stmts.insertUser.run(op.user); } catch (e) {}
break;
case 'login':
stmts.updateLogin.run(1, op.userId);
break;
case 'logout':
stmts.updateLogin.run(0, op.userId);
break;
case 'updateProfile':
stmts.updateProfile.run({
id: op.data.id || op.data.userId,
username: op.data.username || '',
name: op.data.name || '',
bio: op.data.bio || '',
date_of_birth: op.data.date_of_birth || '',
is_profile_public: op.data.is_profile_public || 'true'
});
break;
case 'updatePhoto':
stmts.updatePhoto.run(op.picUrl, op.userId);
break;
}
}
});
transaction();
return { processed: operations.length };
}
return;
}
// ============ MAIN THREAD ============
const fastify = require('fastify')({
logger: false,
bodyLimit: 1048576,
keepAliveTimeout: 61000,
connectionTimeout: 61000,
maxRequestsPerSocket: 0,
requestTimeout: 30000
});
const cors = require('@fastify/cors');
const multipart = require('@fastify/multipart');
const fastifyStatic = require('@fastify/static');
const crypto = require('crypto');
const fs = require('fs');
const fsp = require('fs').promises;
const path = require('path');
let sharp;
try { sharp = require('sharp'); } catch (e) {}
const MAX_BUCKET_SIZE = 60 * 1024 * 1024 * 1024;
const MAX_USERS = 30000;
const MAX_PHOTO_SIZE_KB = 20;
const MAX_RPS = 15000;
const BATCH_SIZE = 15000;
const HASH = process.env.HASH || 'v3l0st-d3f4ult-h4sh-k3y-2024!@#$%^&*()';
const SPACE_NAME = process.env.SPACE_NAME || '2';
const MAX_USERNAME_LENGTH = 200;
const MAX_NAME_LENGTH = 200;
const MAX_PASSWORD_LENGTH = 200;
const MAX_BIO_LENGTH = 1000;
let totalUsers = 0;
let currentRPS = 0;
let rpsWindowStart = Date.now();
let isSpaceFull_RPS = false;
let isSpaceFull_Users = false;
const DATA_DIR = process.env.DATA_DIR || "/data";
const PHOTOS_DIR = path.join(DATA_DIR, "photos");
const STATUS_DIR = process.env.STATUS_DIR || "/status";
const FULL_FILE = path.join(STATUS_DIR, "full.txt");
const SIZE_FILE = path.join(STATUS_DIR, "size.txt");
[DATA_DIR, PHOTOS_DIR, STATUS_DIR].forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});
if (!fs.existsSync(FULL_FILE)) fs.writeFileSync(FULL_FILE, '', 'utf8');
if (!fs.existsSync(SIZE_FILE)) fs.writeFileSync(SIZE_FILE, '', 'utf8');
async function getFullSpaces() {
try {
const content = await fsp.readFile(FULL_FILE, 'utf8');
return content.trim() ? content.trim().split('|') : [];
} catch (e) { return []; }
}
async function addSpaceToFull() {
try {
let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => '');
content = content.trim();
let spaces = content ? content.split('|') : [];
if (!spaces.includes(SPACE_NAME)) {
spaces.push(SPACE_NAME);
await fsp.writeFile(FULL_FILE, spaces.join('|'), 'utf8');
}
} catch (e) {}
}
async function removeSpaceFromFull() {
try {
let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => '');
content = content.trim();
let spaces = content ? content.split('|') : [];
const newSpaces = spaces.filter(s => s !== SPACE_NAME);
if (newSpaces.length !== spaces.length) {
await fsp.writeFile(FULL_FILE, newSpaces.join('|'), 'utf8');
}
} catch (e) {}
}
async function addSpaceToSize() {
try {
let content = await fsp.readFile(SIZE_FILE, 'utf8').catch(() => '');
content = content.trim();
let spaces = content ? content.split('|') : [];
if (!spaces.includes(SPACE_NAME)) {
spaces.push(SPACE_NAME);
await fsp.writeFile(SIZE_FILE, spaces.join('|'), 'utf8');
}
} catch (e) {}
}
function checkRPSStatus() {
const now = Date.now();
if (now - rpsWindowStart >= 1000) {
if (currentRPS >= MAX_RPS * 0.9 && !isSpaceFull_RPS) {
isSpaceFull_RPS = true;
addSpaceToFull();
} else if (currentRPS < MAX_RPS * 0.7 && isSpaceFull_RPS) {
isSpaceFull_RPS = false;
removeSpaceFromFull();
}
currentRPS = 0;
rpsWindowStart = now;
}
}
async function checkUserCountStatus() {
try {
const count = await dbRequest('getCount');
if (count >= MAX_USERS && !isSpaceFull_Users) {
isSpaceFull_Users = true;
await addSpaceToSize();
await addSpaceToFull();
}
} catch (e) {}
}
let dbWorker;
let dbRequestId = 0;
const dbPromises = new Map();
function initDBWorker() {
return new Promise((resolve, reject) => {
dbWorker = new ThreadWorker(__filename, { workerData: { dataDir: DATA_DIR } });
dbWorker.on('message', (message) => {
const { id, success, result, error } = message;
const promise = dbPromises.get(id);
if (promise) {
dbPromises.delete(id);
if (success) promise.resolve(result);
else promise.reject(new Error(error));
}
});
dbWorker.on('error', reject);
dbWorker.on('online', resolve);
});
}
function dbRequest(action, data = {}) {
return new Promise((resolve, reject) => {
const id = ++dbRequestId;
dbPromises.set(id, { resolve, reject });
dbWorker.postMessage({ id, action, data });
});
}
class FastRateLimiter {
constructor(maxRequests = 15000, windowMs = 1000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = 0;
this.windowStart = Date.now();
}
isAllowed() {
const now = Date.now();
if (now - this.windowStart >= this.windowMs) {
this.requests = 0;
this.windowStart = now;
}
if (this.requests >= this.maxRequests) return false;
this.requests++;
currentRPS++;
return true;
}
}
const rateLimiter = new FastRateLimiter(15000, 1000);
function hashPassword(password) {
return crypto.createHash('sha256').update(HASH + password + HASH).digest('hex');
}
function verifyPassword(password, hash) {
return hashPassword(password) === hash;
}
function generateId() {
return crypto.randomBytes(16).toString('base64url').substring(0, 21);
}
function getBucketSize() {
let totalSize = 0;
try {
const files = fs.readdirSync(PHOTOS_DIR);
for (const file of files) {
try { totalSize += fs.statSync(path.join(PHOTOS_DIR, file)).size; } catch (e) {}
}
try { totalSize += fs.statSync(path.join(DATA_DIR, 'users.db')).size; } catch (e) {}
} catch (e) {}
return totalSize;
}
async function compressToWebp(fileData, maxSizeKb = 20) {
if (!sharp) return fileData;
try {
let image = sharp(fileData);
const metadata = await image.metadata();
const currentSizeKb = fileData.length / 1024;
if (metadata.format === 'webp' && currentSizeKb <= maxSizeKb) return fileData;
let quality = 80;
let outputBuffer;
while (quality >= 10) {
outputBuffer = await image
.resize(800, 800, { fit: 'inside', withoutEnlargement: true })
.webp({ quality })
.toBuffer();
if (outputBuffer.length / 1024 <= maxSizeKb) break;
quality -= 10;
}
if (outputBuffer.length / 1024 > maxSizeKb) {
outputBuffer = await image
.resize(400, 400, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 30 })
.toBuffer();
}
return outputBuffer;
} catch (e) { return fileData; }
}
async function setupFastify() {
await fastify.register(cors, { origin: '*' });
await fastify.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } });
await fastify.register(fastifyStatic, { root: PHOTOS_DIR, prefix: '/photos/', maxAge: 86400000 });
fastify.addHook('onRequest', async (request, reply) => {
if (!rateLimiter.isAllowed()) {
reply.status(503).send({ error: 'rate_limit' });
return;
}
});
fastify.post('/signup', async (request, reply) => {
const { username, password, name } = request.body || {};
if (!username || !password || !name) {
reply.status(400).send({ detail: 'Missing fields' });
return;
}
const cleanUsername = username.trim().toLowerCase();
if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) {
reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` });
return;
}
if (password.length < 6 || password.length > MAX_PASSWORD_LENGTH) {
reply.status(400).send({ detail: `Password must be 6-${MAX_PASSWORD_LENGTH} chars` });
return;
}
if (name.trim().length > MAX_NAME_LENGTH) {
reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` });
return;
}
const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername });
if (existingUser) {
reply.status(409).send({ detail: 'Username exists' });
return;
}
const count = await dbRequest('getCount');
if (count >= MAX_USERS) {
reply.status(507).send({ detail: 'Server full' });
return;
}
const userId = generateId();
const newUser = {
id: userId,
username: cleanUsername,
password_hash: hashPassword(password),
name: name.trim(),
created_at: new Date().toISOString(),
is_login: 0,
bio: '',
pic_url: '',
date_of_birth: '',
is_profile_public: 'true'
};
try {
await dbRequest('insertUser', { user: newUser });
totalUsers = count + 1;
await checkUserCountStatus();
reply.send({ success: true, user_id: userId, username: cleanUsername });
} catch (e) {
reply.status(500).send({ detail: 'Signup failed' });
}
});
fastify.post('/login', async (request, reply) => {
const { username, password } = request.body || {};
if (!username || !password) {
reply.status(400).send({ detail: 'Missing fields' });
return;
}
const cleanUsername = username.trim().toLowerCase();
const user = await dbRequest('getUserByUsername', { username: cleanUsername });
if (!user) {
reply.status(401).send({ detail: 'Invalid credentials' });
return;
}
if (!verifyPassword(password, user.password_hash)) {
reply.status(401).send({ detail: 'Invalid credentials' });
return;
}
await dbRequest('updateLogin', { userId: user.id, isLogin: true });
reply.send({
success: true,
id: user.id,
username: user.username,
name: user.name,
bio: user.bio || '',
pic_url: user.pic_url || '',
date_of_birth: user.date_of_birth || '',
is_profile_public: user.is_profile_public || 'true',
created_at: user.created_at
});
});
fastify.post('/logout', async (request, reply) => {
const { id } = request.body || {};
if (!id) {
reply.status(400).send({ detail: 'Missing ID' });
return;
}
const user = await dbRequest('getUserById', { userId: id });
if (!user) {
reply.status(404).send({ detail: 'Not found' });
return;
}
await dbRequest('updateLogin', { userId: id, isLogin: false });
reply.send({ success: true });
});
fastify.put('/updateprofiledetails', async (request, reply) => {
const { id, username, name, bio, date_of_birth, is_profile_public } = request.body || {};
if (!id) {
reply.status(400).send({ detail: 'Missing ID' });
return;
}
const user = await dbRequest('getUserById', { userId: id });
if (!user) {
reply.status(404).send({ detail: 'Not found' });
return;
}
if (name && name.length > MAX_NAME_LENGTH) {
reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` });
return;
}
if (bio && bio.length > MAX_BIO_LENGTH) {
reply.status(400).send({ detail: `Bio max ${MAX_BIO_LENGTH} chars` });
return;
}
let cleanUsername = '';
if (username && username.trim() !== '') {
cleanUsername = username.trim().toLowerCase();
if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) {
reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` });
return;
}
if (cleanUsername !== user.username) {
const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername });
if (existingUser) {
reply.status(409).send({ detail: 'Username exists' });
return;
}
}
}
await dbRequest('updateProfile', {
userId: id,
username: cleanUsername,
name: name || '',
bio: bio !== undefined ? bio : '',
date_of_birth: date_of_birth || '',
is_profile_public: is_profile_public || 'true'
});
reply.send({ success: true });
});
fastify.post('/uploadphoto', async (request, reply) => {
if (getBucketSize() >= MAX_BUCKET_SIZE) {
reply.status(507).send({ detail: 'Storage full' });
return;
}
try {
const data = await request.file();
if (!data || !data.mimetype.startsWith('image/')) {
reply.status(400).send({ detail: 'Invalid image' });
return;
}
const fileBuffer = await data.toBuffer();
const id = (data.fields && data.fields.id) ? data.fields.id.value : null;
const user = await dbRequest('getUserById', { userId: id });
if (!id || !user) {
reply.status(404).send({ detail: 'Not found' });
return;
}
const compressed = await compressToWebp(fileBuffer, MAX_PHOTO_SIZE_KB);
const filename = `${id}_${Math.floor(Date.now() / 1000)}.webp`;
await fsp.writeFile(path.join(PHOTOS_DIR, filename), compressed);
await dbRequest('updatePhoto', { userId: id, picUrl: `/photos/${filename}` });
reply.send({ success: true });
} catch (e) {
reply.status(500).send({ detail: 'Failed' });
}
});
fastify.get('/getuser', async (request, reply) => {
const { id, username } = request.query || {};
let user;
if (username) {
user = await dbRequest('getUserByUsername', { username: username.toLowerCase() });
} else if (id) {
user = await dbRequest('getUserById', { userId: id });
} else {
reply.status(400).send({ detail: 'Missing params' });
return;
}
if (!user) {
reply.status(404).send({ detail: 'Not found' });
return;
}
reply.send({
id: user.id,
username: user.username,
name: user.name,
bio: user.bio || '',
pic_url: user.pic_url || '',
date_of_birth: user.date_of_birth || '',
is_profile_public: user.is_profile_public || 'true',
created_at: user.created_at,
is_login: Boolean(user.is_login)
});
});
fastify.get('/check', async (request, reply) => {
const count = await dbRequest('getCount');
reply.send({
status: 'ok',
count: count,
maxUsers: MAX_USERS,
isFull: count >= MAX_USERS,
spaceName: SPACE_NAME
});
});
return fastify;
}
async function start() {
try {
await initDBWorker();
totalUsers = await dbRequest('getCount');
await setupFastify();
setInterval(() => { checkRPSStatus(); }, 100);
setInterval(async () => { await checkUserCountStatus(); }, 5000);
const port = process.env.PORT || 7860;
await fastify.listen({ port, host: '0.0.0.0' });
console.log(`Server running on port ${port}`);
} catch (err) {
console.error('Startup error:', err.message);
process.exit(1);
}
}
process.on('SIGTERM', async () => {
await removeSpaceFromFull();
await fastify.close();
if (dbWorker) await dbWorker.terminate();
process.exit(0);
});
process.on('SIGINT', async () => {
await removeSpaceFromFull();
await fastify.close();
if (dbWorker) await dbWorker.terminate();
process.exit(0);
});
start().catch(err => process.exit(1));