add rollback option to admintools

This commit is contained in:
HF 2020-06-22 22:49:33 +02:00
parent a69711d838
commit 423b603542
3 changed files with 333 additions and 28 deletions

View File

@ -13,6 +13,8 @@ const keptState = {
coords: null,
tlcoords: null,
brcoords: null,
tlrcoords: null,
brrcoords: null,
};
async function submitImageAction(
@ -57,6 +59,27 @@ async function submitProtAction(
callback(await resp.text());
}
async function submitRollback(
date,
canvas,
tlcoords,
brcoords,
callback,
) {
const data = new FormData();
const timeString = date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2);
data.append('rollback', timeString);
data.append('canvasid', canvas);
data.append('ulcoor', tlcoords);
data.append('brcoor', brcoords);
const resp = await fetch('./admintools', {
credentials: 'include',
method: 'POST',
body: data,
});
callback(await resp.text());
}
async function submitIPAction(
action,
callback,
@ -78,13 +101,23 @@ function Admintools({
canvasId,
canvases,
}) {
const curDate = new Date();
let day = curDate.getDate();
let month = curDate.getMonth() + 1;
if (month < 10) month = `0${month}`;
if (day < 10) day = `0${day}`;
const maxDate = `${curDate.getFullYear()}-${month}-${day}`;
const [selectedCanvas, selectCanvas] = useState(canvasId);
const [imageAction, selectImageAction] = useState('build');
const [iPAction, selectIPAction] = useState('ban');
const [protAction, selectProtAction] = useState('protect');
const [date, selectDate] = useState(maxDate);
const [coords, selectCoords] = useState(keptState.coords);
const [tlcoords, selectTLCoords] = useState(keptState.tlcoords);
const [brcoords, selectBRCoords] = useState(keptState.brcoords);
const [tlrcoords, selectTLRCoords] = useState(keptState.tlrcoords);
const [brrcoords, selectBRRCoords] = useState(keptState.brrcoords);
const [resp, setResp] = useState(null);
const [submitting, setSubmitting] = useState(false);
@ -129,8 +162,6 @@ function Admintools({
</span>
</div>
)}
<h3 className="modaltitle">Image Upload</h3>
<p className="modalcotext">Upload images to canvas</p>
<p className="modalcotext">Choose Canvas:&nbsp;
<select
onChange={(e) => {
@ -154,6 +185,10 @@ function Admintools({
}
</select>
</p>
<br />
<div className="modaldivider" />
<h3 className="modaltitle">Image Upload</h3>
<p className="modalcotext">Upload images to canvas</p>
<p className="modalcotext">
File:&nbsp;
<input type="file" name="image" id="imgfile" />
@ -221,29 +256,6 @@ function Admintools({
(if you need finer grained control,&nbsp;
use protect with image upload and alpha layers)
</p>
<p className="modalcotext">Choose Canvas:&nbsp;
<select
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{
Object.keys(canvases).map((canvas) => ((canvases[canvas].v)
? null
: (
<option
selected={canvas === selectedCanvas}
value={canvas}
>
{
canvases[canvas].title
}
</option>
)))
}
</select>
</p>
<select
onChange={(e) => {
const sel = e.target;
@ -317,6 +329,80 @@ function Admintools({
{(submitting) ? '...' : 'Submit'}
</button>
<br />
<div className="modaldivider" />
<h3 className="modaltitle">Rollback to Date</h3>
<p className="modalcotext">
Rollback an area of the canvas to a set date (00:00 UTC)
</p>
<input
type="date"
value={date}
requiredPattern="\d{4}-\d{2}-\d{2}"
min={canvases[selectedCanvas].sd}
max={maxDate}
onChange={(evt) => {
selectDate(evt.target.value);
}}
/>
<p className="modalcotext">
Top-left corner (X_Y):&nbsp;
<input
value={tlrcoords}
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
type="text"
placeholder="X_Y"
onChange={(evt) => {
const co = evt.target.value.trim();
selectTLRCoords(co);
keptState.tlrcoords = co;
}}
/>
</p>
<p className="modalcotext">
Bottom-right corner (X_Y):&nbsp;
<input
value={brrcoords}
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
type="text"
placeholder="X_Y"
onChange={(evt) => {
const co = evt.target.value.trim();
selectBRRCoords(co);
keptState.brrcoords = co;
}}
/>
</p>
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitRollback(
date,
selectedCanvas,
tlrcoords,
brrcoords,
(ret) => {
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(submitting) ? '...' : 'Submit'}
</button>
<br />
<div className="modaldivider" />
<h3 className="modaltitle">IP Actions</h3>

115
src/core/rollback.js Normal file
View File

@ -0,0 +1,115 @@
/*
* Rolls back an area of the canvas to a specific date
*
* @flow
*/
// Tile creation is allowed to be slow
/* eslint-disable no-await-in-loop */
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
import RedisCanvas from '../data/models/RedisCanvas';
import logger from './logger';
import { getChunkOfPixel } from './utils';
import Palette from './Palette';
import { TILE_SIZE } from './constants';
import { BACKUP_DIR } from './config';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
export default async function rollbackToDate(
canvasId: number,
x: number,
y: number,
width: number,
height: number,
date: string,
) {
if (!BACKUP_DIR) {
return 0;
}
const dir = path.resolve(__dirname, BACKUP_DIR);
const backupDir = `${dir}/${date}/${canvasId}/tiles`;
if (!fs.existsSync(backupDir)) {
return 0;
}
logger.info(
`Rollback area ${width}/${height} to ${x}/${y}/${canvasId} to date ${date}`,
);
const canvas = canvases[canvasId];
const { colors, size } = canvas;
const palette = new Palette(colors);
const canvasMinXY = -(size / 2);
const [ucx, ucy] = getChunkOfPixel(size, x, y);
const [lcx, lcy] = getChunkOfPixel(size, x + width, y + height);
let totalPxlCnt = 0;
logger.info(`Loading to chunks from ${ucx} / ${ucy} to ${lcx} / ${lcy} ...`);
let chunk;
let empty = false;
let emptyBackup = false;
let backupChunk;
for (let cx = ucx; cx <= lcx; cx += 1) {
for (let cy = ucy; cy <= lcy; cy += 1) {
chunk = await RedisCanvas.getChunk(canvasId, cx, cy);
if (chunk && chunk.length === TILE_SIZE * TILE_SIZE) {
chunk = new Uint8Array(chunk);
empty = false;
} else {
chunk = new Uint8Array(TILE_SIZE * TILE_SIZE);
empty = true;
}
try {
emptyBackup = false;
backupChunk = await sharp(`${backupDir}/${cx}/${cy}.png`)
.ensureAlpha()
.raw()
.toBuffer();
backupChunk = new Uint32Array(backupChunk.buffer);
} catch {
logger.info(
// eslint-disable-next-line max-len
`Backup chunk ${backupDir}/${cx}/${cy}.png could not be loaded, assuming empty.`,
);
emptyBackup = true;
}
let pxlCnt = 0;
if (!empty || !emptyBackup) {
// offset of chunk in image
const cOffX = cx * TILE_SIZE + canvasMinXY - x;
const cOffY = cy * TILE_SIZE + canvasMinXY - y;
let cOff = 0;
for (let py = 0; py < TILE_SIZE; py += 1) {
for (let px = 0; px < TILE_SIZE; px += 1) {
const clrX = cOffX + px;
const clrY = cOffY + py;
if (clrX >= 0 && clrY >= 0 && clrX < width && clrY < height) {
const pixel = (emptyBackup)
? 0 : palette.abgr.indexOf(backupChunk[cOff]);
if (pixel !== -1) {
chunk[cOff] = pixel;
pxlCnt += 1;
}
}
cOff += 1;
}
}
}
if (pxlCnt) {
const ret = await RedisCanvas.setChunk(cx, cy, chunk, canvasId);
if (ret) {
logger.info(`Loaded ${pxlCnt} pixels into chunk ${cx}, ${cy}.`);
totalPxlCnt += pxlCnt;
}
}
chunk = null;
}
}
logger.info('Rollback done.');
return totalPxlCnt;
}

View File

@ -29,6 +29,7 @@ import {
imageABGR2Canvas,
protectCanvasArea,
} from '../core/Image';
import rollbackCanvasArea from '../core/rollback';
const router = express.Router();
@ -229,7 +230,7 @@ async function executeImageAction(
* Execute actions for protecting areas
* @param action what to do
* @param ulcoor coords of upper-left corner in X_Y format
* @param brcoord coords of bottom-right corner in X_Y format
* @param brcoor coords of bottom-right corner in X_Y format
* @param canvasid numerical canvas id as string
* @return [ret, msg] http status code and message
*/
@ -274,8 +275,6 @@ async function executeProtAction(
error = 'No imageaction given';
} else if (!canvas) {
error = 'Invalid canvas selected';
} else if (!canvases[canvasid]) {
error = 'Invalid canvas selected';
} else if (action !== 'protect' && action !== 'unprotect') {
error = 'Invalid action (must be protect or unprotect)';
}
@ -319,6 +318,98 @@ async function executeProtAction(
];
}
/*
* Execute rollback
* @param date in format YYYYMMdd
* @param ulcoor coords of upper-left corner in X_Y format
* @param brcoor coords of bottom-right corner in X_Y format
* @param canvasid numerical canvas id as string
* @return [ret, msg] http status code and message
*/
async function executeRollback(
date: string,
ulcoor: string,
brcoor: string,
canvasid: number,
) {
if (!ulcoor || !brcoor) {
return [403, 'Not all coordinates defined'];
}
if (!canvasid) {
return [403, 'canvasid not defined'];
}
let splitCoords = ulcoor.trim().split('_');
if (splitCoords.length !== 2) {
return [403, 'Invalid Coordinate Format for top-left corner'];
}
const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
splitCoords = brcoor.trim().split('_');
if (splitCoords.length !== 2) {
return [403, 'Invalid Coordinate Format for bottom-right corner'];
}
const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
const canvas = canvases[canvasid];
let error = null;
if (Number.isNaN(x)) {
error = 'x of top-left corner is not a valid number';
} else if (Number.isNaN(y)) {
error = 'y of top-left corner is not a valid number';
} else if (Number.isNaN(u)) {
error = 'x of bottom-right corner is not a valid number';
} else if (Number.isNaN(v)) {
error = 'y of bottom-right corner is not a valid number';
} else if (u < x || v < y) {
error = 'Corner coordinates are alligned wrong';
} else if (!date) {
error = 'No date given';
} else if (Number.isNaN(Number(date))) {
error = 'Invalid date';
} else if (!canvas) {
error = 'Invalid canvas selected';
}
if (error !== null) {
return [403, error];
}
const canvasMaxXY = canvas.size / 2;
const canvasMinXY = -canvasMaxXY;
if (x < canvasMinXY || y < canvasMinXY
|| x >= canvasMaxXY || y >= canvasMaxXY) {
return [403, 'Coordinates of top-left corner are outside of canvas'];
}
if (u < canvasMinXY || v < canvasMinXY
|| u >= canvasMaxXY || v >= canvasMaxXY) {
return [403, 'Coordinates of bottom-right corner are outside of canvas'];
}
const width = u - x + 1;
const height = v - y + 1;
if (width * height > 1000000) {
return [403, 'Can not rollback more than 1m pixels at onec'];
}
const pxlCount = await rollbackCanvasArea(
canvasid,
x,
y,
width,
height,
date,
);
logger.info(
// eslint-disable-next-line max-len
`ADMINTOOLS: Rollback to ${date} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
);
return [
200,
// eslint-disable-next-line max-len
`Successfully rolled back ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
];
}
/*
* Check for POST parameters,
@ -351,6 +442,19 @@ router.post('/', upload.single('image'), async (req, res, next) => {
);
res.status(ret).send(msg);
return;
} if (req.body.rollback) {
// rollback is date as YYYYMMdd
const {
rollback, ulcoor, brcoor, canvasid,
} = req.body;
const [ret, msg] = await executeRollback(
rollback,
ulcoor,
brcoor,
canvasid,
);
res.status(ret).send(msg);
return;
}
next();