]> git.ardis.lu - targz/commitdiff
Initial commit
authorArdis Lu <ardisglu@gmail.com>
Fri, 3 May 2024 08:40:27 +0000 (01:40 -0700)
committerArdis Lu <ardisglu@gmail.com>
Fri, 3 May 2024 08:40:27 +0000 (01:40 -0700)
README.md [new file with mode: 0644]
index.html [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..25ba487
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# targz
+
+Web utility to make a `.tar.gz` file from one or more input files.
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..e549e38
--- /dev/null
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="icon" href="data:,">
+  <title>targz</title>
+  <style>
+    body {
+      font-family: sans-serif;
+      background-color: hsl(216deg 50% 93%);
+      color: hsl(216deg 50% 10%);
+      margin: 0;
+      block-size: 100dvb;
+      display: grid;
+      place-content: center;
+    }
+
+    form {
+      display: flex;
+      flex-direction: column;
+      padding: 16px;
+      border-radius: 8px;
+      border: 1px solid hsl(216deg 50% 75%);
+
+      & * {
+        inline-size: fit-content;
+      }
+    }
+
+    label {
+      font-size: 1.75rem;
+      font-weight: 300;
+    }
+
+    input {
+      font-size: 1.25rem;
+    }
+
+    button {
+      font-size: 1.25rem;
+      background-color: hsl(216deg 70% 80%);
+      padding: 4px 8px;
+      margin-block-start: 16px;
+      border-radius: 4px;
+      border: 1px solid hsl(216deg 75% 75%);
+      cursor: pointer;
+      transition: background-color 400ms;
+
+      &:hover,
+      &:focus {
+        transition: background-color 200ms;
+        background-color: hsl(216deg 70% 85%);
+      }
+    }
+  </style>
+</head>
+
+<body>
+  <main>
+    <form>
+      <label for="contents">Files</label>
+      <input type="file" id="contents" name="contents" multiple required>
+      <button>Create .tar.gz file</button>
+    </form>
+  </main>
+
+  <script type="module">
+    async function tar(files) {
+      const encoder = new TextEncoder();
+      const buffer = [];
+      for (const file of files) {
+        // Add the file header
+        const header = [
+          ...encoder.encode(file.name.padEnd(100, '\0')), // 100 bytes: File name
+          ...encoder.encode('0000777\0'), // 8 bytes: File mode (octal)
+          ...encoder.encode('0000000\0'), // 8 bytes: Owner's numeric user ID (octal)
+          ...encoder.encode('0000000\0'), // 8 bytes: Group's numeric user ID (octal)
+          ...encoder.encode(`${file.size.toString(8).padStart(11, '0')}\0`), // 12 bytes: File size in bytes (octal)
+          ...encoder.encode(`${Math.floor(file.lastModified / 1000).toString(8).padStart(11, '0')}\0`), // 12 bytes: Last modification time in numeric Unix time format (octal)
+          ...encoder.encode('        '), // 8 bytes: Checksum for header record
+          ...encoder.encode('0'), // 1 byte: Link indicator (file type)
+          ...encoder.encode('\0'.repeat(100)), // 100 bytes: Name of linked file
+          ...encoder.encode('ustar  '), // 8 bytes: magic number 'ustar  '
+          ...encoder.encode('\0'.repeat(32)), // 32 bytes: Owner user name
+          ...encoder.encode('\0'.repeat(32)), // 32 bytes: Owner group name
+          ...encoder.encode('\0'.repeat(8)), // 8 bytes: Device major number
+          ...encoder.encode('\0'.repeat(8)), // 8 bytes: Device minor number
+          ...encoder.encode('\0'.repeat(155)), // 155 bytes: Filename prefix
+          ...encoder.encode('\0'.repeat(13)), // 13 bytes: empty padding to reach 512 bytes
+        ];
+        const checksum = header.reduce((a, b) => a + b);
+        header.splice(148, 8, ...encoder.encode(`${checksum.toString(8).padStart(6, '0')}\0 `));
+        buffer.push(...header);
+
+        // Add the file data, with end padded to the nearest 512 byte chunk
+        const chunkSize = 512 * Math.ceil((file.size ? file.size : 1) / 512);
+        buffer.push(
+          ...new Uint8Array(await file.arrayBuffer()),
+          ...Array(chunkSize - file.size).fill('\0')
+        )
+      }
+
+      // End of tar is marked by 1024 null bytes
+      buffer.push(...encoder.encode('\0'.repeat(1024)));
+
+      return Uint8Array.from(buffer);
+    }
+
+    async function gzip(byteArray) {
+      const compressed = new Blob([byteArray]).stream().pipeThrough(new CompressionStream('gzip'));
+      return new Uint8Array(await new Response(compressed).arrayBuffer());
+    }
+
+    function download(byteArray) {
+      const file = new File([byteArray], `${Date.now()}.tar.gz`, { type: 'application/gzip' });
+      const a = document.createElement('a');
+      a.href = URL.createObjectURL(file);
+      a.download = file.name;
+      a.click();
+    }
+
+    document.querySelector('form').addEventListener('submit', e => {
+      e.preventDefault();
+      const files = e.target.elements['contents'].files;
+      tar(files).then(gzip).then(download);
+    });
+  </script>
+</body>
+
+</html>
\ No newline at end of file
This page took 0.036744 seconds and 4 git commands to generate.