Skip to content

Commit

Permalink
Add restore command and keep translations when updating (#9966)
Browse files Browse the repository at this point in the history
* add restore command and keep translations when updating

* add --test flag

* add parameter to prevent accidental runs
  • Loading branch information
riknoll authored Apr 12, 2024
1 parent f9e3edc commit ab7902d
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 2 deletions.
13 changes: 12 additions & 1 deletion cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7092,7 +7092,18 @@ ${pxt.crowdin.KEY_VARIABLE} - crowdin key

advancedCommand("augmentdocs", "test markdown docs replacements", augmnetDocsAsync, "<temlate.md> <doc.md>");

advancedCommand("crowdin", "upload, download, clean, stats files to/from crowdin", pc => crowdin.execCrowdinAsync.apply(undefined, pc.args), "<cmd> <path> [output]")
p.defineCommand({
name: "crowdin",
advanced: true,
argString: "<cmd> <path> [output]",
help: "upload, download, clean, stats files to/from crowdin",
flags: {
test: { description: "test run, do not upload files to crowdin" }
}
}, pc => {
if (pc.flags.test) pxt.crowdin.setTestMode();
return crowdin.execCrowdinAsync.apply(undefined, pc.args)
})

advancedCommand("hidlist", "list HID devices", hid.listAsync)
advancedCommand("hidserial", "run HID serial forwarding", hid.serialAsync, undefined, true);
Expand Down
41 changes: 40 additions & 1 deletion cli/crowdin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'path';
import Map = pxt.Map;

import * as commandParser from './commandparser';
import { downloadFileTranslationsAsync, getFileProgressAsync, listFilesAsync, uploadFileAsync } from './crowdinApi';
import { downloadFileTranslationsAsync, getFileProgressAsync, listFilesAsync, restoreFileBefore, uploadFileAsync } from './crowdinApi';

export function uploadTargetTranslationsAsync(parsed?: commandParser.ParsedCommand) {
const uploadDocs = parsed && !!parsed.flags["docs"];
Expand Down Expand Up @@ -229,6 +229,15 @@ export async function execCrowdinAsync(cmd: string, ...args: string[]): Promise<
}
await execDownloadAsync(args[0], args[1]);
break;
case "restore":
if (!args[0]) {
throw new Error("Time missing");
}
if (args[1] !== "force" && !pxt.crowdin.testMode) {
throw new Error(`Refusing to run restore command without 'force' argument. Re-run as 'pxt crowdin restore <date> force' to proceed or use --test flag to test.`);
}
execRestoreFiles(args[0]);
break;
default:
throw new Error("unknown command");
}
Expand Down Expand Up @@ -371,3 +380,33 @@ async function execStatsAsync(language?: string) {
console.log(`blocks\t ${language}\t ${(translated / phrases * 100) >> 0}%\t ${(approved / phrases * 100) >> 0}%\t ${phrases}\t ${translated}\t ${approved}`)
}
}

async function execRestoreFiles(time: string | number) {
let cutoffTime;

if (!isNaN(parseInt(time + ""))) {
cutoffTime = parseInt(time + "");
}
else {
cutoffTime = new Date(time).getTime();
}

const crowdinDir = pxt.appTarget.id;

// If this is run inside pxt-core, give results for all targets
const isCore = crowdinDir === "core";

const files = await listFilesAsync();

for (const file of files) {
pxt.debug("Processing file: " + file + "...");

// Files for core are in the top-level of the crowdin project
const isCoreFile = file.indexOf("/") === -1;


if ((isCore && !isCoreFile) || !file.startsWith(crowdinDir + "/")) continue;

await restoreFileBefore(file, cutoffTime);
}
}
75 changes: 75 additions & 0 deletions cli/crowdinApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ async function getAllFiles() {
}

async function createFile(fileName: string, fileContent: any, directoryId?: number): Promise<void> {
if (pxt.crowdin.testMode) return;

const { uploadStorageApi, sourceFilesApi } = getClient();

// This request happens in two parts: first we upload the file to the storage API,
Expand All @@ -318,6 +320,8 @@ async function createFile(fileName: string, fileContent: any, directoryId?: numb
}

async function createDirectory(dirName: string, directoryId?: number): Promise<SourceFilesModel.Directory> {
if (pxt.crowdin.testMode) return undefined;

const { sourceFilesApi } = getClient();

const dir = await sourceFilesApi.createDirectory(projectId, {
Expand All @@ -332,14 +336,85 @@ async function createDirectory(dirName: string, directoryId?: number): Promise<S
return dir.data;
}

export async function restoreFileBefore(filename: string, cutoffTime: number) {
const revisions = await listFileRevisions(filename);

let lastRevision: SourceFilesModel.FileRevision;
let lastRevisionBeforeCutoff: SourceFilesModel.FileRevision;

for (const rev of revisions) {
const time = new Date(rev.date).getTime();

if (lastRevision) {
if (time > new Date(lastRevision.date).getTime()) {
lastRevision = rev;
}
}
else {
lastRevision = rev;
}

if (time < cutoffTime) {
if (lastRevisionBeforeCutoff) {
if (time > new Date(lastRevisionBeforeCutoff.date).getTime()) {
lastRevisionBeforeCutoff = rev;
}
}
else {
lastRevisionBeforeCutoff = rev;
}
}
}

if (lastRevision === lastRevisionBeforeCutoff) {
pxt.log(`${filename} already at most recent valid revision before ${formatTime(cutoffTime)}`);
}
else if (lastRevisionBeforeCutoff) {
pxt.log(`Restoring ${filename} to revision ${formatTime(new Date(lastRevisionBeforeCutoff.date).getTime())}`)
await restorefile(lastRevisionBeforeCutoff.fileId, lastRevisionBeforeCutoff.id);
}
else {
pxt.log(`No revisions found for ${filename} before ${formatTime(cutoffTime)}`);
}
}

function formatTime(time: number) {
const date = new Date(time);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
}

async function listFileRevisions(filename: string): Promise<SourceFilesModel.FileRevision[]> {
const { sourceFilesApi } = getClient();

const fileId = await getFileIdAsync(filename);
const revisions = await sourceFilesApi
.withFetchAll()
.listFileRevisions(projectId, fileId);

return revisions.data.map(rev => rev.data);
}


async function updateFile(fileId: number, fileName: string, fileContent: any): Promise<void> {
if (pxt.crowdin.testMode) return;

const { uploadStorageApi, sourceFilesApi } = getClient();

const storageResponse = await uploadStorageApi.addStorage(fileName, fileContent);

await sourceFilesApi.updateOrRestoreFile(projectId, fileId, {
storageId: storageResponse.data.id,
updateOption: "keep_translations"
});
}

async function restorefile(fileId: number, revisionId: number) {
if (pxt.crowdin.testMode) return;

const { sourceFilesApi } = getClient();

await sourceFilesApi.updateOrRestoreFile(projectId, fileId, {
revisionId
});
}

Expand Down

0 comments on commit ab7902d

Please sign in to comment.