add rollback option to admintools
This commit is contained in:
parent
a69711d838
commit
423b603542
|
@ -13,6 +13,8 @@ const keptState = {
|
||||||
coords: null,
|
coords: null,
|
||||||
tlcoords: null,
|
tlcoords: null,
|
||||||
brcoords: null,
|
brcoords: null,
|
||||||
|
tlrcoords: null,
|
||||||
|
brrcoords: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function submitImageAction(
|
async function submitImageAction(
|
||||||
|
@ -57,6 +59,27 @@ async function submitProtAction(
|
||||||
callback(await resp.text());
|
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(
|
async function submitIPAction(
|
||||||
action,
|
action,
|
||||||
callback,
|
callback,
|
||||||
|
@ -78,13 +101,23 @@ function Admintools({
|
||||||
canvasId,
|
canvasId,
|
||||||
canvases,
|
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 [selectedCanvas, selectCanvas] = useState(canvasId);
|
||||||
const [imageAction, selectImageAction] = useState('build');
|
const [imageAction, selectImageAction] = useState('build');
|
||||||
const [iPAction, selectIPAction] = useState('ban');
|
const [iPAction, selectIPAction] = useState('ban');
|
||||||
const [protAction, selectProtAction] = useState('protect');
|
const [protAction, selectProtAction] = useState('protect');
|
||||||
|
const [date, selectDate] = useState(maxDate);
|
||||||
const [coords, selectCoords] = useState(keptState.coords);
|
const [coords, selectCoords] = useState(keptState.coords);
|
||||||
const [tlcoords, selectTLCoords] = useState(keptState.tlcoords);
|
const [tlcoords, selectTLCoords] = useState(keptState.tlcoords);
|
||||||
const [brcoords, selectBRCoords] = useState(keptState.brcoords);
|
const [brcoords, selectBRCoords] = useState(keptState.brcoords);
|
||||||
|
const [tlrcoords, selectTLRCoords] = useState(keptState.tlrcoords);
|
||||||
|
const [brrcoords, selectBRRCoords] = useState(keptState.brrcoords);
|
||||||
const [resp, setResp] = useState(null);
|
const [resp, setResp] = useState(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
@ -129,8 +162,6 @@ function Admintools({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h3 className="modaltitle">Image Upload</h3>
|
|
||||||
<p className="modalcotext">Upload images to canvas</p>
|
|
||||||
<p className="modalcotext">Choose Canvas:
|
<p className="modalcotext">Choose Canvas:
|
||||||
<select
|
<select
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
@ -154,6 +185,10 @@ function Admintools({
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
<div className="modaldivider" />
|
||||||
|
<h3 className="modaltitle">Image Upload</h3>
|
||||||
|
<p className="modalcotext">Upload images to canvas</p>
|
||||||
<p className="modalcotext">
|
<p className="modalcotext">
|
||||||
File:
|
File:
|
||||||
<input type="file" name="image" id="imgfile" />
|
<input type="file" name="image" id="imgfile" />
|
||||||
|
@ -221,29 +256,6 @@ function Admintools({
|
||||||
(if you need finer grained control,
|
(if you need finer grained control,
|
||||||
use protect with image upload and alpha layers)
|
use protect with image upload and alpha layers)
|
||||||
</p>
|
</p>
|
||||||
<p className="modalcotext">Choose Canvas:
|
|
||||||
<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
|
<select
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const sel = e.target;
|
const sel = e.target;
|
||||||
|
@ -317,6 +329,80 @@ function Admintools({
|
||||||
{(submitting) ? '...' : 'Submit'}
|
{(submitting) ? '...' : 'Submit'}
|
||||||
</button>
|
</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):
|
||||||
|
<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):
|
||||||
|
<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 />
|
<br />
|
||||||
<div className="modaldivider" />
|
<div className="modaldivider" />
|
||||||
<h3 className="modaltitle">IP Actions</h3>
|
<h3 className="modaltitle">IP Actions</h3>
|
||||||
|
|
115
src/core/rollback.js
Normal file
115
src/core/rollback.js
Normal 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;
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import {
|
||||||
imageABGR2Canvas,
|
imageABGR2Canvas,
|
||||||
protectCanvasArea,
|
protectCanvasArea,
|
||||||
} from '../core/Image';
|
} from '../core/Image';
|
||||||
|
import rollbackCanvasArea from '../core/rollback';
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
@ -229,7 +230,7 @@ async function executeImageAction(
|
||||||
* Execute actions for protecting areas
|
* Execute actions for protecting areas
|
||||||
* @param action what to do
|
* @param action what to do
|
||||||
* @param ulcoor coords of upper-left corner in X_Y format
|
* @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
|
* @param canvasid numerical canvas id as string
|
||||||
* @return [ret, msg] http status code and message
|
* @return [ret, msg] http status code and message
|
||||||
*/
|
*/
|
||||||
|
@ -274,8 +275,6 @@ async function executeProtAction(
|
||||||
error = 'No imageaction given';
|
error = 'No imageaction given';
|
||||||
} else if (!canvas) {
|
} else if (!canvas) {
|
||||||
error = 'Invalid canvas selected';
|
error = 'Invalid canvas selected';
|
||||||
} else if (!canvases[canvasid]) {
|
|
||||||
error = 'Invalid canvas selected';
|
|
||||||
} else if (action !== 'protect' && action !== 'unprotect') {
|
} else if (action !== 'protect' && action !== 'unprotect') {
|
||||||
error = 'Invalid action (must be protect or 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,
|
* Check for POST parameters,
|
||||||
|
@ -351,6 +442,19 @@ router.post('/', upload.single('image'), async (req, res, next) => {
|
||||||
);
|
);
|
||||||
res.status(ret).send(msg);
|
res.status(ret).send(msg);
|
||||||
return;
|
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();
|
next();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user