diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fc52d5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: CI CD Workflow + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Run test script + run: | + echo "$FUNCIONA_VARIAVEIS" + ls -lhas + echo "Nome da imagem weilplace-api" + echo "$IMG_WEILPLACE_API" + echo "Nome da imagem weilplace-site" + echo "$IMG_WEILPLACE_SITE" + echo "REF: ${{ github.ref }}" + + - name: Set up environment + run: mv ./backend/.env.example ./backend/.env + + - name: Install and run tests + run: | + cd ./backend + npm ci + npm run test + # https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images + build: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + + # https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up environment + run: mv ./backend/.env.example ./backend/.env + + - name: Extract metadata (tags, labels) for Docker + id: apimeta + uses: docker/metadata-action@v5 + with: + images: erickweil/weilplace-api + + - name: Build and push (backend) + uses: docker/build-push-action@v5 + with: + context: ./backend + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.apimeta.outputs.tags }} + labels: ${{ steps.apimeta.outputs.labels }} + + + - name: Extract metadata (tags, labels) for Docker + id: sitemeta + uses: docker/metadata-action@v5 + with: + images: erickweil/weilplace-site + + - name: Build and push (frontend) + uses: docker/build-push-action@v5 + with: + context: ./frontend + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.sitemeta.outputs.tags }} + labels: ${{ steps.sitemeta.outputs.labels }} \ No newline at end of file diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..edc0468 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,3 @@ +.kompose/ +*.secret* +.vercel \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example index bcf3978..9bf3bfd 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,7 +21,7 @@ WEBSOCKET_ENABLED="true" # Usado para deploy na Vercel # Irá utilizar redis para guardar a imagem -DISABLE_FILESYSTEM="false" - -# Se ativado, irá chamar o pixel-saver para salvar a imagem a cada X mudanças +# DISABLE_FILESYSTEM="false" +# Ao chamar get /picture, irá obter a imagem, aplicar todas as mudanças pendentes, e retornar a imagem +# Isto é para permitir que o backend não precise do serviço do pixel saver # PIXEL_SAVER_CALL="true" \ No newline at end of file diff --git a/backend/.vercelignore b/backend/.vercelignore new file mode 100644 index 0000000..7355cf2 --- /dev/null +++ b/backend/.vercelignore @@ -0,0 +1,3 @@ +public/* +*.secret* +node_modules/ \ No newline at end of file diff --git a/backend/service_pixelsaver.js b/backend/service_pixelsaver.js index 35447eb..2bbc16a 100644 --- a/backend/service_pixelsaver.js +++ b/backend/service_pixelsaver.js @@ -1,6 +1,6 @@ import { REDIS_ENABLED } from "./src/config/options.js"; -import PixelSaver from "./src/service/pixelSaver.js"; +import { PixelSaver } from "./src/service/pixelSaver.js"; import { connectToRedis } from "./src/config/redisConnection.js"; import PixelChanges from "./src/controller/pixelChanges.js"; @@ -12,8 +12,9 @@ if(!REDIS_ENABLED) { let redisClient = await connectToRedis(PixelChanges.getLuaScriptsConfig()); await PixelChanges.init(redisClient); -const cronTask = await PixelSaver.init(); +const saver = await PixelSaver.init(); +const cronJob = saver.scheduleCron(); -if(!cronTask) { - console.error("Erro ao inicial o Pixel Saver: Não iniciou a cron task"); +if(!cronJob) { + throw new Error("Erro ao iniciar o cronJob"); } \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js index 2a94ec7..79e243a 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -4,13 +4,13 @@ import session from "express-session"; import RedisStore from "connect-redis"; import http from "http"; -import { SESSION_MAX_AGE, SESSION_SECRET, REDIS_ENABLED, WEBSOCKET_ENABLED } from "./config/options.js"; +import { SESSION_MAX_AGE, SESSION_SECRET, REDIS_ENABLED, WEBSOCKET_ENABLED, PIXEL_SAVER_CALL } from "./config/options.js"; import routes from "./routes/index.js"; import { SessionManager } from "./middleware/sessionManager.js"; import PixelChanges from "./controller/pixelChanges.js"; import { connectToRedis } from "./config/redisConnection.js"; -import PixelSaver from "./service/pixelSaver.js"; +import { PixelSaver } from "./service/pixelSaver.js"; import initWebSocketServer from "./websocket/websocket.js"; let redisClient = REDIS_ENABLED ? await connectToRedis(PixelChanges.getLuaScriptsConfig()) : false; @@ -19,8 +19,11 @@ await PixelChanges.init(redisClient); if(!REDIS_ENABLED) { - console.log("Iniciando saver na mesma instância já que o redis está desativado..."); - const cronTask = await PixelSaver.init(); + if(!PIXEL_SAVER_CALL) { + console.log("Iniciando saver na mesma instância já que o redis está desativado..."); + const saver = await PixelSaver.init(); + const cronJob = saver.scheduleCron(); + } } const app = express(); @@ -78,7 +81,7 @@ let sessionOptions = { resave: true, saveUninitialized: true, cookie: { - sameSite: "strict", + sameSite: "none", httpOnly: true, secure: true, maxAge: SESSION_MAX_AGE*1000, // em milissegundos @@ -103,7 +106,8 @@ if(REDIS_ENABLED) { const sessionParser = session(sessionOptions); app.use(sessionParser); -app.use(SessionManager.initSession); +// Não. só vai ter sessão se estiver logado +// app.use(SessionManager.initSession); // Websockets if(WEBSOCKET_ENABLED) { diff --git a/backend/src/config/options.js b/backend/src/config/options.js index 542f6aa..78d7fbe 100644 --- a/backend/src/config/options.js +++ b/backend/src/config/options.js @@ -26,4 +26,5 @@ export const REQUIRE_GOOGLE_LOGIN = envOrDefault("REQUIRE_GOOGLE_LOGIN","false") export const OAUTH2_CLIENT_ID = envOrDefault("OAUTH2_CLIENT_ID",""); export const PALLETE = palleteJson.pallete.map(c => parseInt(c,16)); -export const DISABLE_FILESYSTEM = envOrDefault("DISABLE_FILESYSTEM","false") === "true"; \ No newline at end of file +export const DISABLE_FILESYSTEM = envOrDefault("DISABLE_FILESYSTEM","false") === "true"; +export const PIXEL_SAVER_CALL = envOrDefault("PIXEL_SAVER_CALL","false") === "true"; \ No newline at end of file diff --git a/backend/src/controller/pixelChanges.js b/backend/src/controller/pixelChanges.js index 0ec0838..02f3960 100644 --- a/backend/src/controller/pixelChanges.js +++ b/backend/src/controller/pixelChanges.js @@ -2,7 +2,6 @@ import { convertChangeToBase64 } from "../config/changesProtocol.js"; import RedisMock from "./redisMock.js"; import { defineScript, createClient, commandOptions } from "redis"; import { DISABLE_FILESYSTEM, IMAGE_HEIGHT, IMAGE_WIDTH, PALLETE } from "../config/options.js"; -import { handleApplyChanges } from "../routes/privateRoutes.js"; let KEY_IDENTIFIER = "changesid"; let KEY_SAVEDINDEX = "savedindex"; @@ -12,6 +11,7 @@ let KEY_PICTURE = "picture"; // a imagem salva (se não usar filesystem) let options = { redis_prefix: "REDIS_PREFIX" in process.env ? process.env.REDIS_PREFIX : "", + // em milissegundos place_delay: "PLACE_DELAY" in process.env ? parseInt(process.env.PLACE_DELAY) : 0, /* O MAX_CHANGES_RESPONSE indica o tamanho de resposta máximo que será dado ao cliente ao solicitar as modificações, mesmo que haja mais. @@ -20,8 +20,7 @@ let options = { Nada impede o cliente de solicitar a string inteira desde o índice 0, para evitar DoS seria bom não deixar esse valor muito alto*/ - max_changes_response: "MAX_CHANGES_RESPONSE" in process.env ? parseInt(process.env.MAX_CHANGES_RESPONSE) : 8192, - pixel_saver_call: "PIXEL_SAVER_CALL" in process.env ? process.env.PIXEL_SAVER_CALL === "true" : false + max_changes_response: "MAX_CHANGES_RESPONSE" in process.env ? parseInt(process.env.MAX_CHANGES_RESPONSE) : 8192 }; /** @type {ReturnType} */ @@ -72,9 +71,6 @@ class PixelChanges { */ static calledInit = false; static async init(_redisClient, _options) { - if(PixelChanges.calledInit) { - throw new Error("PixelChanges já foi inicializado, deve chamá-lo apenas uma vez."); - } PixelChanges.calledInit = true; if(_options) { @@ -143,6 +139,7 @@ class PixelChanges { // Possível problema: Como primeiro pega o tempo e depois seta o pixel, // se mandar requisições simultâneas pode acontecer de setar mais de um pixel devido // à condição de corrida de verificarem ao mesmo tempo? + // A fazer: converter essa lógica para lua script, para garantir que seja atômico dentro do redis if(options.place_delay > 0) { let timeLastPlaced = await redisClient.HGET(KEY_USERTIMES,username); @@ -177,16 +174,9 @@ class PixelChanges { await redisClient.APPEND(KEY_CHANGES, changes); - // 1 a cada 100 vezes chama o pixel saver - if(options.pixel_saver_call && Math.random() > 0.99) { - setTimeout(async () => { - console.log(await handleApplyChanges()); - },0); - } - return { message: "OK", - delay: options.place_delay + delay: options.place_delay > 0 ? Math.ceil(options.place_delay/1000) : 0 }; } @@ -204,7 +194,7 @@ class PixelChanges { return { message: "OK", identifier: ret[0], - i: ret[1] + i: parseInt(ret[1]) }; } diff --git a/backend/src/middleware/sessionManager.js b/backend/src/middleware/sessionManager.js index 8c99f3a..3c8f086 100644 --- a/backend/src/middleware/sessionManager.js +++ b/backend/src/middleware/sessionManager.js @@ -18,6 +18,10 @@ export const haiku = () =>{ export class SessionManager { + /** + * @deprecated + * Descontinuado pois causa inúmeros nomes de usuário no redis quando o cookie não está configurado direito + */ static initSession(req,res,next) { if(!req.session.username) { @@ -92,11 +96,7 @@ export class SessionManager { return userinfo; } else { - if(!hasSession) { - return undefined; - } - - return userinfo; + return { username: "anonimo" }; } } } \ No newline at end of file diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index 9844060..1120a80 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -13,7 +13,7 @@ import { LOG_ROUTES } from "../config/options.js"; export const logRoutes = (req,res,next) => { const timestamp = new Date().toISOString(); - const username = req.session?.username; + const username = req.session?.username || "anonimo"; let ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || diff --git a/backend/src/routes/pixelsRoutes.js b/backend/src/routes/pixelsRoutes.js index 16e0175..9348076 100644 --- a/backend/src/routes/pixelsRoutes.js +++ b/backend/src/routes/pixelsRoutes.js @@ -1,10 +1,11 @@ import express from "express"; import path from "path"; import PixelChanges from "../controller/pixelChanges.js"; -import { DISABLE_FILESYSTEM, PATH_PICTURE } from "../config/options.js"; +import { DISABLE_FILESYSTEM, PATH_PICTURE, PIXEL_SAVER_CALL, REDIS_ENABLED } from "../config/options.js"; import { genericRouteHandler } from "../middleware/routeHandler.js"; import { SessionManager } from "../middleware/sessionManager.js"; import { palleteJson } from "../config/pallete.js"; +import { PixelSaver } from "../service/pixelSaver.js"; const router = express.Router(); @@ -320,11 +321,51 @@ router.get("/picture", async (req,res) => { }; if(DISABLE_FILESYSTEM) { - let { body } = await handleGetPicureFromRedis(); + let response = await handleGetPicureFromRedis(); + let pngBuffer; + if(response.status === 200) { + pngBuffer = response.body; + } + + // Isso causa aplicar todas as mudanças que ocorreram desde o último save, e fazer um novo save se necessário + // Tornando desnescessário o uso de cronjob para salvar a imagem, porém confiando que o cliente irá atualizar a página de vez em quando + if(PIXEL_SAVER_CALL) { + const { status, json } = await handleGetChanges({i:""+resp.i}); + if(status !== 200) throw new Error("Não foi possível obter mudanças:"+json); + + // Só vai aplicar a imagem se realmente houve mudanças + if(!pngBuffer || json.identifier !== resp.identifier || json.i !== resp.i) { + const saver = await PixelSaver.init(pngBuffer, resp.i, resp.identifier); + + // Força execução de todas as mudanças + const result = await saver.cronTask(json); + + // Se houve mudanças, a imagem foi salva, retornando o buffer png da nova imagem aqui para evitar acessar denovo + if(result && (result instanceof Buffer)) { + pngBuffer = result; + } + + // pode acontecer quando é uma imagem nova, tenta pegar do redis denovo + if(!pngBuffer) { + response = await handleGetPicureFromRedis(); + if(response.status === 200) { + pngBuffer = response.body; + } + } + + headers["X-Changes-Offset"] = saver.last_i; + headers["X-Changes-Identifier"] = saver.last_identifier; + } + } + + if(!pngBuffer) { + return res.status(500).json({message: "Erro ao acessar imagem"}); + } + return res .setHeaders(new Map(Object.entries(headers))) .contentType("image/png") - .status(200).send(body); + .status(200).send(pngBuffer); } else { // 1. Se entre o getChanges acima e a leitura da imagem, for salva uma nova imagem // não causa problemas, pois o cliente irá re-aplicar as mudanças, um re-trabalho diff --git a/backend/src/routes/privateRoutes.js b/backend/src/routes/privateRoutes.js index 9ff0d5c..58da26f 100644 --- a/backend/src/routes/privateRoutes.js +++ b/backend/src/routes/privateRoutes.js @@ -5,10 +5,8 @@ import express from "express"; import PixelChanges from "../controller/pixelChanges.js"; -import { API_SHARED_SECRET, REDIS_ENABLED } from "../config/options.js"; +import { API_SHARED_SECRET } from "../config/options.js"; import { genericRouteHandler } from "../middleware/routeHandler.js"; -import { connectToRedis } from "../config/redisConnection.js"; -import PixelSaver from "../service/pixelSaver.js"; const router = express.Router(); @@ -88,32 +86,6 @@ export const handleSavePicture = async (body) => { }; }; -// Utilizado apenas quando não tem o serviço pixel saver -// para aplicar as mudanças na imagem e salvar -export const handleApplyChanges = async () => { - if(!REDIS_ENABLED) { - throw new Error("Quando não utiliza redis o serviço do pixel saver é iniciado na mesma instância da api."); - } - - // Iniciar conexão com o redis e ligar o controller - if(!PixelChanges.calledInit) { - let redisClient = await connectToRedis(PixelChanges.getLuaScriptsConfig()); - await PixelChanges.init(redisClient); - } - - if(!PixelSaver.calledInit) { - await PixelSaver.init({ delay_cron_save: 0 }); - } - - // Força execução de todas as mudanças - await PixelSaver.cronTask(); - - return { - status: 200, - json: {message: "OK"} - }; -}; - /*router.post("/setsavedindex",secretMiddleware, async (req,res) => { const resp = await handleSetSavedIndex(req.body); return res.status(resp.status).json(resp.json); @@ -127,6 +99,5 @@ router.post("/resetchanges",secretMiddleware, async (req,res) => { router.post("/setsavedindex", secretMiddleware, genericRouteHandler("POST","/setsavedindex",false,handleSetSavedIndex)); router.post("/resetchanges", secretMiddleware, genericRouteHandler("POST","/resetchanges",false,handleResetChanges)); router.post("/savepicture", secretMiddleware, genericRouteHandler("POST","/savepicture",false,handleSavePicture)); -router.post("/applychanges", secretMiddleware, genericRouteHandler("POST","/applychanges",false,handleApplyChanges)); export default router; \ No newline at end of file diff --git a/backend/src/service/pixelSaver.js b/backend/src/service/pixelSaver.js index 4ead2a8..dc3d4d4 100644 --- a/backend/src/service/pixelSaver.js +++ b/backend/src/service/pixelSaver.js @@ -11,7 +11,7 @@ import PixelHistory from "../controller/pixelHistory.js"; // redis_prefix: "REDIS_PREFIX" in process.env ? process.env.REDIS_PREFIX : "", // Pega do env ou então utiliza os valores padrão. -let options = { +const defaultOptions = { delay_cron_save: "DELAY_CRON_SAVE" in process.env ? parseInt(process.env.DELAY_CRON_SAVE) : 10, /* O MAX_CHANGES_SIZE controla quando resetar a string de modificações causando todos os clientes a re-baixar a imagem. O PixelSaver que faz @@ -23,53 +23,82 @@ let options = { save_history: "SAVE_HISTORY" in process.env ? process.env.SAVE_HISTORY === "true" : true, }; -let imgPixelsBuff = false; -let last_i = -1; -let last_identifier = false; - -class PixelSaver { - static async loadImage() { - let imgSharpObj; - if(DISABLE_FILESYSTEM) { - let picturePngBuff = await PixelSaver.doPictureRedisGet(); - imgSharpObj = await sharp(picturePngBuff); - } else { - imgSharpObj = await sharp(PATH_PICTURE); - } - +export class PixelSaver { + static async _loadImage(pngBuff) { + let imgSharpObj = await sharp(pngBuff); let imgMetadata = await imgSharpObj.metadata(); - - imgPixelsBuff = await imgSharpObj.raw().toBuffer(); + let loadedBuffer = await imgSharpObj.raw().toBuffer(); console.log("Metadata:", imgMetadata); if(imgMetadata.width != IMAGE_WIDTH || imgMetadata.height != IMAGE_HEIGHT) { throw new Error(`Imagem deveria ser ${IMAGE_WIDTH}x${IMAGE_HEIGHT} porém é ${imgMetadata.width}x${imgMetadata.height} abortando...`); } - - return imgSharpObj; + return loadedBuffer; } - static calledInit = false; - static async init(_options) { - if(PixelSaver.calledInit) { - throw new Error("PixelSaver já foi inicializado, deve ser chamado apenas uma vez"); - } - PixelSaver.calledInit = true; + static async doChangesGet(i) { + const res = await handleGetChanges({i:""+i}); + if(res.status == 200) return res.json; + else throw new Error("Não foi possível completar a requisição:"+res); + } - if(_options) { - options = {...options, ..._options}; - } + static async doSavedIndexGet() { + const res = await handleGetSavedIndex(); + if(res.status == 200) return res.json; + else throw new Error("Não foi possível completar a requisição:"+res); + } - let imgSharpObj = false; - + static async doSetSavedIndexPost(saved_i) { + const res = await handleSetSavedIndex({i:""+saved_i}); + if(res.status == 200) return res.json; + else throw new Error("Não foi possível completar a requisição:"+res); + } + + static async doResetChangesPost(trim_i) { + const res = await handleResetChanges({i:""+trim_i}); + if(res.status == 200) return res.json; + else throw new Error("Não foi possível completar a requisição:"+res); + } + + static async doSavePicturePost(picture) { + const res = await handleSavePicture({picture: picture}); + if(res.status == 200) return res.json; + else throw new Error("Não foi possível completar a requisição:"+res); + } + + //options; + //** @type {Buffer} */ + //imgPixelsBuff; + //** @type {number} */ + //last_i; + //** @type {string|null} */ + //last_identifier; + static async init(pngBuffer = null, last_i = -1, last_identifier = null, _options = defaultOptions) { + let imgPixelsBuff; try { - imgSharpObj = await PixelSaver.loadImage(); - } catch (e) { - console.log(e); + if(DISABLE_FILESYSTEM || pngBuffer) { + if(!pngBuffer) { + let responseGet = await handleGetPicureFromRedis(); + if(responseGet.status != 200) { + throw new Error("Erro ao pegar a imagem do redis:"+JSON.stringify(responseGet, null, 2)); + } + pngBuffer = responseGet.body; + } + + imgPixelsBuff = await PixelSaver._loadImage(pngBuffer); + } else { + imgPixelsBuff = await PixelSaver._loadImage(PATH_PICTURE); + } + } catch(e) { + console.error("Erro ao carregar a imagem: ",e); + } + + let newImage = false; + if(!imgPixelsBuff) { console.log("Criando nova imagem, pois não havia nenhuma"); - imgSharpObj = await sharp({ + let imgSharpObj = await sharp({ create: { width: IMAGE_WIDTH, height: IMAGE_HEIGHT, @@ -79,40 +108,61 @@ class PixelSaver { }); imgPixelsBuff = await imgSharpObj.raw().toBuffer(); - - await PixelSaver.savePicture(last_identifier, last_i); + newImage = true; + } + + const saver = new PixelSaver(imgPixelsBuff, last_i, last_identifier, _options); + + if(newImage) { + await saver.savePicture(); } + + return saver; + } + + constructor(imgPixelsBuff, last_i = -1, last_identifier = null, _options = defaultOptions) { + this.options = {...defaultOptions, ..._options}; - console.log("Image(%s) [%d,%d] Pallete size:%d, Save Delay:%d",PATH_PICTURE,IMAGE_WIDTH,IMAGE_HEIGHT,PALLETE.length,options.delay_cron_save); + this.imgPixelsBuff = imgPixelsBuff; + this.last_i = last_i; + this.last_identifier = last_identifier; + console.log("Image(%s) [%d,%d] Pallete size:%d, Save Delay:%d",PATH_PICTURE,IMAGE_WIDTH,IMAGE_HEIGHT,PALLETE.length,this.options.delay_cron_save); + } + + scheduleCron() { // delay negativo ou igual a 0 desativa o auto-save - if(options.delay_cron_save > 0) + if(this.options.delay_cron_save > 0) { // https://www.npmjs.com/package/node-cron#cron-syntax - return cron.schedule("*/"+options.delay_cron_save+" * * * * *", PixelSaver.cronTask); - } else return false; + return cron.schedule("*/"+this.options.delay_cron_save+" * * * * *", () => { + this.cronTask(); + }); + } } - static async cronTask() { - if(!imgPixelsBuff) return; + async cronTask(changesResp = null) { + if(!this.imgPixelsBuff) return null; let repeat = true; - let before_i = last_i; + let before_i = this.last_i; while(repeat === true) { - repeat = await PixelSaver.queryChanges(); + repeat = await this.queryChanges(changesResp); } // Só salva a imagem se houver mudanças - if(before_i != -1 && before_i != last_i) { - await PixelSaver.automaticSave(); + if(before_i != -1 && before_i != this.last_i) { + return await this.automaticSave(); + } else { + return null; } } - static setSinglePixel(x,y,rgb) { + setSinglePixel(x,y,rgb) { const index = y * IMAGE_HEIGHT + x; - imgPixelsBuff[index * 3 + 0] = (rgb >> 16) & 0xFF; - imgPixelsBuff[index * 3 + 1] = (rgb >> 8) & 0xFF; - imgPixelsBuff[index * 3 + 2] = (rgb) & 0xFF; + this.imgPixelsBuff[index * 3 + 0] = (rgb >> 16) & 0xFF; + this.imgPixelsBuff[index * 3 + 1] = (rgb >> 8) & 0xFF; + this.imgPixelsBuff[index * 3 + 2] = (rgb) & 0xFF; } /*async getPicture() { @@ -129,9 +179,9 @@ class PixelSaver { return buffer; }*/ - static async savePicture(identifier, i) { + async savePicture() { let imgSharpObj = sharp( - imgPixelsBuff, { + this.imgPixelsBuff, { raw: { width: IMAGE_WIDTH, height: IMAGE_HEIGHT, @@ -152,69 +202,37 @@ class PixelSaver { })*/ if(DISABLE_FILESYSTEM) { // Obtêm o Buffer da imagem - imgSharpObj = await imgSharpObj.toBuffer(); + let imgPngBuffer = await imgSharpObj.toBuffer(); // envia o Buffer para o redis - await PixelSaver.doSavePicturePost(imgSharpObj); + await PixelSaver.doSavePicturePost(imgPngBuffer); + + return imgPngBuffer; } else { - imgSharpObj = await imgSharpObj.toFile(PATH_PICTURE); - } + let fileInfo = await imgSharpObj.toFile(PATH_PICTURE); - if(options.save_history) - { - await PixelHistory.saveHistoryPicture(); + if(this.options.save_history) + { + await PixelHistory.saveHistoryPicture(); + } + + return fileInfo; } } - static applyChangesOnImage(changes) { + applyChangesOnImage(changes) { return handlePixelChanges( changes, PALLETE, (rgb,x,y) => { - PixelSaver.setSinglePixel(x,y,rgb); + this.setSinglePixel(x,y,rgb); } ); } - static async doChangesGet(i) { - const res = await handleGetChanges({i:""+i}); - if(res.status == 200) return res.json; - else throw new Error("Não foi possível completar a requisição:"+res); - } - - static async doSavedIndexGet() { - const res = await handleGetSavedIndex(); - if(res.status == 200) return res.json; - else throw new Error("Não foi possível completar a requisição:"+res); - } - - static async doSetSavedIndexPost(saved_i) { - const res = await handleSetSavedIndex({i:""+saved_i}); - if(res.status == 200) return res.json; - else throw new Error("Não foi possível completar a requisição:"+res); - } - - static async doResetChangesPost(trim_i) { - const res = await handleResetChanges({i:""+trim_i}); - if(res.status == 200) return res.json; - else throw new Error("Não foi possível completar a requisição:"+res); - } - - static async doSavePicturePost(picture) { - const res = await handleSavePicture({picture: picture}); - if(res.status == 200) return res.json; - else throw new Error("Não foi possível completar a requisição:"+res); - } - - static async doPictureRedisGet() { - const res = await handleGetPicureFromRedis(); - if(res.status == 200) return res.body; - else throw new Error("Não foi possível completar a requisição:"+JSON.stringify(res,null,2)); - } - // retorna verdadeiro se precisa repetir logo em seguida - static async queryChanges () { + async queryChanges(changesResp = null) { try { - const resp = last_i >= 0 ? await PixelSaver.doChangesGet(last_i) : await PixelSaver.doSavedIndexGet(); + const resp = changesResp || (this.last_i >= 0 ? await PixelSaver.doChangesGet(this.last_i) : await PixelSaver.doSavedIndexGet()); if(!resp || resp.i === undefined @@ -231,18 +249,18 @@ class PixelSaver { // resetou o stream de mudanças // Tem que ver isso depois, ta certo colocar 0??? - if(last_identifier !== false && identifier != last_identifier) { + if(this.last_identifier && identifier != this.last_identifier) { console.log("Atualizou identifier. resetando index"); - last_i = 0; - last_identifier = identifier; + this.last_i = 0; + this.last_identifier = identifier; return false; } - if(i != last_i) { - last_i = i; - last_identifier = identifier; + if(i != this.last_i) { + this.last_i = i; + this.last_identifier = identifier; - if(PixelSaver.applyChangesOnImage(changes)) { + if(this.applyChangesOnImage(changes)) { const pixelsModificados = Math.floor(changes.length/8); console.log("Teve %d pixels modificados",pixelsModificados); @@ -262,26 +280,25 @@ class PixelSaver { } } - static async automaticSave() { + async automaticSave() { try { console.log("Salvando imagem..."); - await PixelSaver.savePicture(last_identifier, last_i); + let imgPngBuff = await this.savePicture(); // Reseta as modificações se ficou muito grande - if(last_i >= options.max_changes_size) { - console.log("Chegou no limite de modificações(i = %d), resetando modificações...",last_i); - const resetresp = await PixelSaver.doResetChangesPost(last_i); + if(this.last_i >= this.options.max_changes_size) { + console.log("Chegou no limite de modificações(i = %d), resetando modificações...",this.last_i); + const resetresp = await PixelSaver.doResetChangesPost(this.last_i); - last_i = 0; // Já que resetou é seguro por 0 né? - last_identifier = resetresp.identifier; + this.last_i = 0; // Já que resetou é seguro por 0 né? + this.last_identifier = resetresp.identifier; } else { - await PixelSaver.doSetSavedIndexPost(last_i); + await PixelSaver.doSetSavedIndexPost(this.last_i); } + + return imgPngBuff; } catch(e) { console.log("Erro ao aplicar as mudanças da imagem:",e); } } - -} - -export default PixelSaver; \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/websocket/websocket.js b/backend/src/websocket/websocket.js index d373425..ec2727d 100644 --- a/backend/src/websocket/websocket.js +++ b/backend/src/websocket/websocket.js @@ -50,7 +50,7 @@ const onConnection = (wss,ws) => { if(LOG_ROUTES) { const timestamp = new Date().toISOString(); - console.log(timestamp+" "+session?.username+" websocket: "+method+" "+route+" ",json); + console.log(timestamp+" "+ws.username+" websocket: "+method+" "+route+" ",json); } const resp = await handler(json,session); @@ -75,23 +75,24 @@ const initWebSocketServer = (server, sessionParser) => { ws.isAlive = true; ws.on("error",(error) => { - console.log("Erro em:"+ws.session?.username,error); + console.log("Erro em:"+ws.username,error); }); ws.on("pong", () => { //console.log("Recebeu pong de "+ws.session.username); ws.isAlive = true; }); ws.on("close", () => { - console.log(ws.session?.username+" desconectou."); + console.log(ws.username+" desconectou."); }); // https://stackoverflow.com/questions/12182651/expressjs-websocket-session-sharing sessionParser(req, {}, function() { - SessionManager.initSession(req, null, () => {}); - + // Não + // SessionManager.initSession(req, null, () => {}); + ws.session = req.session; - console.log(ws.session?.username+" conectado."); - + ws.username = req.session?.username || haiku(); + console.log(ws.username+" conectado."); onConnection(wss,ws); }); @@ -101,7 +102,7 @@ const initWebSocketServer = (server, sessionParser) => { const interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) { - console.log("Fechando conexão quebrada de "+ws.session?.username); + console.log("Fechando conexão quebrada de "+ws.username); return ws.terminate(); // ws.readyState !== WebSocket.OPEN } diff --git a/backend/test/pixels.test.js b/backend/test/pixels.test.js index 71335fb..f4d1aa8 100644 --- a/backend/test/pixels.test.js +++ b/backend/test/pixels.test.js @@ -39,7 +39,7 @@ describe("Testando Rota Pixels",() => { test("Setando pixel", async () => { await PixelChanges.init(redisMock, { - place_delay: 100, + place_delay: 100000, }); expect(await handlePostPixel({x:"0",y:"0",c:"0"},session)).toMatchObject({ diff --git a/docker-compose.yml b/docker-compose.yml index 55ca37d..caebf22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,10 +10,12 @@ services: environment: PORT: "3001" PATH_PICTURE: "./public/pixels/picture.png" - LOG_ROUTES: "false" + LOG_ROUTES: "true" REDIS_ENABLED: "true" REDIS_URL: "redis://weilplace-redis:6379" - PLACE_DELAY: 0 + PLACE_DELAY: 1000 + DISABLE_FILESYSTEM: "false" + PIXEL_SAVER_CALL: "false" networks: weilplace: ipv4_address: 10.5.0.2 @@ -31,6 +33,8 @@ services: PATH_PICTURE: "./public/pixels/picture.png" REDIS_ENABLED: "true" REDIS_URL: "redis://weilplace-redis:6379" + DISABLE_FILESYSTEM: "false" + PIXEL_SAVER_CALL: "false" networks: weilplace: ipv4_address: 10.5.0.3 diff --git a/frontend/.vercelignore b/frontend/.vercelignore new file mode 100644 index 0000000..33822cc --- /dev/null +++ b/frontend/.vercelignore @@ -0,0 +1,4 @@ +.next/ +public/* +*.secret* +node_modules/ \ No newline at end of file