Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Client-Side Pre-Upload Package Validation #1061

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions builder/src/components/DragDropFileInput.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import React, {
CSSProperties,
MutableRefObject,
useEffect,
useState,
} from "react";

interface DragDropFileInputProps {
title: string;
onChange?: (files: FileList) => void;
readonly?: boolean;
fileInputRef: MutableRefObject<HTMLInputElement | null>;
}

export const DragDropFileInput: React.FC<DragDropFileInputProps> = (props) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const fileInput = props.fileInputRef.current;
const [fileDropStyle, setFileDropStyle] = useState<CSSProperties>({});
const [lastTarget, setLastTarget] = useState<EventTarget | null>(null);
const [isDragging, setIsDragging] = useState<boolean>(false);
Expand Down Expand Up @@ -39,8 +45,7 @@ export const DragDropFileInput: React.FC<DragDropFileInputProps> = (props) => {
};
const fileChange = () => {
if (!props.readonly) {
const inp = fileInputRef.current;
const files = inp?.files;
const files = fileInput?.files;
if (props.onChange && files) {
props.onChange(files);
}
Expand All @@ -49,9 +54,8 @@ export const DragDropFileInput: React.FC<DragDropFileInputProps> = (props) => {
};
const onDrop = (e: React.DragEvent) => {
if (!props.readonly) {
const inp = fileInputRef.current;
if (inp) {
inp.files = e.dataTransfer.files;
if (fileInput) {
fileInput.files = e.dataTransfer.files;
}
if (props.onChange) {
props.onChange(e.dataTransfer.files);
Expand Down Expand Up @@ -92,7 +96,7 @@ export const DragDropFileInput: React.FC<DragDropFileInputProps> = (props) => {
type="file"
name="newfile"
style={{ display: "none" }}
ref={fileInputRef}
ref={props.fileInputRef}
onChange={fileChange}
disabled={props.readonly}
/>
Expand Down
233 changes: 228 additions & 5 deletions builder/src/upload.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Sentry from "@sentry/browser";
import React, { useEffect, useState } from "react";
import React, { useRef, useEffect, useState } from "react";
import {
Community,
ExperimentalApi,
Expand All @@ -19,6 +19,7 @@ import { FormSelectField } from "./components/FormSelectField";
import { CommunityCategorySelector } from "./components/CommunitySelector";
import { FormRow } from "./components/FormRow";
import { SubmitPackage } from "./api/packageSubmit";
import { BlobReader, ZipReader } from "./vendor/zip-fs-full";

function getUploadProgressBarcolor(uploadStatus: FileUploadStatus | undefined) {
if (uploadStatus == FileUploadStatus.CANCELED) {
Expand All @@ -43,16 +44,15 @@ function getSubmissionProgressBarcolor(
}

class FormErrors {
fileError: string | null = null;
teamError: string | null = null;
communitiesError: string | null = null;
categoriesError: string | null = null;
nsfwError: string | null = null;
generalErrors: string[] = [];
fileErrors: string[] = [];

get hasErrors(): boolean {
return !(
this.fileError == null &&
this.teamError == null &&
this.communitiesError == null &&
this.categoriesError == null &&
Expand Down Expand Up @@ -81,6 +81,7 @@ const SubmissionForm: React.FC<SubmissionFormProps> = observer((props) => {
const [formErrors, setFormErrors] = useState<FormErrors>(new FormErrors());
const [file, setFile] = useState<File | null>(null);
const [fileUpload, setFileUpload] = useState<FileUpload | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [
submissionStatus,
setSubmissionStatus,
Expand Down Expand Up @@ -115,6 +116,12 @@ const SubmissionForm: React.FC<SubmissionFormProps> = observer((props) => {
if (fileUpload) {
await fileUpload.cancelUpload();
}

const input = fileInputRef.current;
if (input) {
input.value = "";
}

setFileUpload(null);
setSubmissionStatus(null);
setFormErrors(new FormErrors());
Expand All @@ -130,13 +137,214 @@ const SubmissionForm: React.FC<SubmissionFormProps> = observer((props) => {
const onFileChange = (files: FileList) => {
const file = files.item(0);
setFile(file);

if (file) {
validateZip(file).then((result) => {
if (result) {
console.log("Zip successfully validated.");
} else {
console.log("Failed to validate zip.");
}
});
}
};

async function validateZip(file: File): Promise<boolean> {
console.log("Selected file: " + file.name);

let errors = new FormErrors();

let blockUpload = false;

let isZip = true;
if (!file.name.toLowerCase().endsWith(".zip")) {
errors.fileErrors.push("The file you selected is not a .zip!");
isZip = false;
blockUpload = true;
}

if (isZip) {
try {
const blobReader = new BlobReader(file);
const zipReader = new ZipReader(blobReader);

const entries = await zipReader.getEntries();

let dllCount = 0;
let hasBepInEx = false;
let hasAssemblyCSharp = false;
let maybeModpack = false;
let noRootFiles = true;
let rootManifest = false;
let hasIcon = false;
let rootIcon = false;
let hasManifest = false;
let hasReadMe = false;
let rootReadMe = false;

for (const entry of entries) {
// console.log(entry.filename);

if (!entry || !(typeof entry.getData === "function")) {
continue;
}

if (entry.filename.toLowerCase().endsWith(".dll")) {
dllCount++;
}

if (
entry.filename.toLowerCase().split("/").pop() ==
"assembly-csharp.dll"
) {
hasAssemblyCSharp = true;
}

if (
entry.filename.toLowerCase().split("/").pop() ==
"bepinex.dll"
) {
hasBepInEx = true;
maybeModpack = true;
}

if (noRootFiles) {
if (!entry.filename.includes("/")) {
noRootFiles = false;
}
}
if (
entry.filename.toLowerCase().endsWith("manifest.json")
) {
hasManifest = true;
if (entry.filename.toLowerCase() == "manifest.json") {
rootManifest = true;
}
}
if (entry.filename.toLowerCase().endsWith("icon.png")) {
hasIcon = true;
if (entry.filename.toLowerCase() == "icon.png") {
rootIcon = true;
}
}
if (entry.filename.toLowerCase().endsWith("readme.md")) {
hasReadMe = true;
if (entry.filename.toLowerCase() == "readme.md") {
rootReadMe = true;
}
}
}

if (hasBepInEx) {
errors.fileErrors.push(
"You have BepInEx.dll in your .zip file. BepInEx should probably be a dependency in your manifest.json file instead."
);
}

if (hasAssemblyCSharp) {
errors.fileErrors.push(
"You have Assembly-CSharp.dll in your .zip file. Your mod may be removed if you do not have permission to distribute this file."
);
}

if (dllCount > 8) {
errors.fileErrors.push(
"You have " +
dllCount +
" .dll files in your .zip file. Some of these files may be unnecessary."
);
maybeModpack = true;
}

if (maybeModpack) {
errors.fileErrors.push(
"If you're making a modpack, do not include the files for each mod in your .zip file. Instead, put the dependency string for each mod inside your manifest.json file."
);
}

if (
noRootFiles &&
hasManifest &&
hasIcon &&
hasReadMe &&
!rootManifest &&
!rootIcon &&
!rootReadMe
) {
blockUpload = true;
errors.fileErrors.push(
"Your manifest, icon, and README files should be at the root of the .zip file. You can prevent this by compressing the contents of a folder, rather than the folder itself."
);
} else {
if (!hasManifest) {
blockUpload = true;
errors.fileErrors.push(
"Your package is missing a manifest.json file!"
);
} else if (!rootManifest) {
blockUpload = true;
errors.fileErrors.push(
"Your manifest.json file is not at the root of the .zip!"
);
}

if (!hasIcon) {
blockUpload = true;
errors.fileErrors.push(
"Your package is missing an icon.png file!"
);
} else if (!rootIcon) {
blockUpload = true;
errors.fileErrors.push(
"Your icon.png file is not at the root of the .zip!"
);
}

if (!hasReadMe) {
blockUpload = true;
errors.fileErrors.push(
"Your package is missing a README.md file!"
);
} else if (!rootReadMe) {
blockUpload = true;
errors.fileErrors.push(
"Your README.md file is not at the root of the .zip!"
);
}
}

await zipReader.close();
} catch (e) {
console.log("Error reading zip: " + e);
return false;
}
}

if (errors.fileErrors.length > 0) {
setFormErrors(errors);

if (blockUpload) {
errors.generalErrors.push(
"An error with your selected file is preventing submission."
);
setSubmissionStatus(SubmissionStatus.ERROR);
return false;
}
return true;
} else {
return true;
}
}

const onSubmit = async (data: any) => {
// TODO: Convert to react-hook-form validation

let fileErrors = formErrors.fileErrors;
setFormErrors(new FormErrors());
const errors = new FormErrors();

errors.fileErrors = fileErrors;

const uploadTeam = data.team ? data.team.value : null;
const uploadCommunities = data.communities
? data.communities.map((com: any) => com.value)
Expand Down Expand Up @@ -245,6 +453,8 @@ const SubmissionForm: React.FC<SubmissionFormProps> = observer((props) => {
(fileUpload?.uploadErrors ?? []).length > 0 ||
formErrors.generalErrors.length > 0;

const hasFileErrors = formErrors.fileErrors.length > 0;

const hasEtagError =
fileUpload &&
fileUpload.uploadErrors.some(
Expand Down Expand Up @@ -296,9 +506,18 @@ const SubmissionForm: React.FC<SubmissionFormProps> = observer((props) => {
title={file ? file.name : "Choose or drag file here"}
onChange={onFileChange}
readonly={!!file}
fileInputRef={fileInputRef}
/>
</div>

{hasFileErrors && (
<div className="mb-0 px-3 py-3 alert alert-info field-errors mt-2">
<ul className="mx-0 my-0 pl-3">
{formErrors.fileErrors.map((e, idx) => (
<li key={`general-${idx}`}>{e}</li>
))}
</ul>
</div>
)}
{currentCommunity != null &&
teams != null &&
communities != null ? (
Expand Down Expand Up @@ -371,7 +590,11 @@ const SubmissionForm: React.FC<SubmissionFormProps> = observer((props) => {

<button
type={"submit"}
disabled={!file || !!fileUpload}
disabled={
!file ||
!!fileUpload ||
submissionStatus == SubmissionStatus.ERROR
}
className="btn btn-primary btn-block"
>
Submit
Expand Down
Loading