add rollback option to admintools
This commit is contained in:
parent
a69711d838
commit
423b603542
|
@ -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:
|
||||
<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:
|
||||
<input type="file" name="image" id="imgfile" />
|
||||
|
@ -221,29 +256,6 @@ function Admintools({
|
|||
(if you need finer grained control,
|
||||
use protect with image upload and alpha layers)
|
||||
</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
|
||||
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):
|
||||
<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 />
|
||||
<div className="modaldivider" />
|
||||
<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,
|
||||
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();
|
||||
|
|
Loading…
Reference in New Issue
Block a user