0.0.4Updated 5 days ago
// deno-lint-ignore-file no-unversioned-import no-import-prefix

import { Args } from "./modules/args.ts";
import html_base from "./src/file_tree.html" with { type: "text" };
import css_base from "./src/file_browser.css" with { type: "text" };
import help_text from "./src/help_text.txt" with { type: "text" };
import help_colors from "./src/help_colors.txt" with { type: "text" };

import local_deno_json from "./deno.json" with { type: "json" };
const local_version = local_deno_json.version;

import { join } from "jsr:@std/path";
import { existsSync } from "jsr:@std/fs";
import { serveFile } from "jsr:@std/http/file-server";
import { GridRenderer } from "./modules/renderers/grid.ts";
import { ListRenderer } from "./modules/renderers/list.ts";
import { Renderer } from "./modules/renderers/_renderer.ts";

ListRenderer;
GridRenderer;

if(Deno.args.includes('void')) Deno.exit(0);

const args = Args.Parse({
  port: {
    type: "number",
    long: "--port",
    short: "-p",
    default: 8080
  },
  help: {
    type: 'flag',
    long: '--help',
    short: '-h'
  },
  hidden: {
    type: 'flag',
    long: '--show-hidden',
    short: '-H'
  }
});

const get_cloud_version = async () => {
  try {
    const cloud_deno_json = await (await fetch('https://viapak.xyz/@utils/http-server/deno.json?direct=true')).json() as typeof local_deno_json;
    return cloud_deno_json.version;
  } catch(_) {
    return local_deno_json.version;
  }
}

const check_for_updates = async () => {
  const cloud_version = await get_cloud_version();
  if(local_version !== cloud_version) {
    do_upgrade().then(() => {
      console.log(`%chttp-server%c has been upgraded to %c${cloud_version}%c!\nRestart %chttp-server%c to use the new version.`, 'color: orange', 'color: skyblue', 'color: orange', 'color: skyblue', 'color: orange', 'color: skyblue');
    })
  }
}

const do_upgrade = () => {
  return new Deno.Command("deno", {
    args: "run --unstable-raw-imports -Arq https://viapak.xyz/@utils/http-server void".split(' ')
  }).output()
}

const upgrade = async (force: boolean = false) => {
  console.log("%cFetching latest %chttp-server%c version...", 'color: skyblue', 'color: orange', 'color: skyblue');

  const cloud_version = await get_cloud_version();

  console.log(`%cLatest version: %c${cloud_version}`, 'color: skyblue', 'color: orange');
  if(local_version == cloud_version) {
    if(!force) {
      console.log(`%cAlready on latest version.\n`);
      return false;
    }

    console.log('%cAlready on latest version. Validating...', 'color: skyblue');
    await do_upgrade();
    console.log('Done\n');
    return true;
  }

  await do_upgrade();

  console.log(`%chttp-server%c has been updated to version %c${cloud_version}%c\n`, 'color: orange', 'color: skyblue', 'color: orange', '');
}

if(Deno.args.includes('upgrade')) {
  await upgrade(true);
  Deno.exit(0);
}

if(args.help || Deno.args.includes('help')) {
  console.log(help_text, ...help_colors.split(/[\r\n]/).map(c => c.length ? `color: ${c}` : ''));
  Deno.exit(0);
}

export interface TreeItem {
  name: string
  path: string
  is_directory: boolean
  href: string
}

let renderer: Renderer = ListRenderer;

export const is_image = (file_name: string) => !!file_name.match(/\.(jpe?g|png|bmp|webp|gif|svg|ico)$/);

setTimeout(check_for_updates, 5000); // 5 seconds after booting
setInterval(check_for_updates, 1000 * 60 * 60 * 2); // Every 2 hours

Deno.serve({
  port: args.port
}, req => {
  const url = new URL(req.url);

  if(url.pathname == '/http-server-mode-switch') {
    renderer = Renderer.ByName[url.searchParams.get('mode') || ''] || ListRenderer;
    return new Response(null, {
      headers: new Headers({
        Location: req.headers.get('referer') || '/'
      }),
      status: 307
    })
  }

  const path = join(Deno.cwd(), url.pathname).replace(/\%20/g, ' ');
  if(!existsSync(path)) {
    return new Response('404 Not Found', { status: 404 });
  }

  const { isDirectory } = Deno.statSync(path);

  if(isDirectory) {
    const index_path = join(path, 'index.html');
    if(existsSync(index_path) && Deno.statSync(index_path).isFile) return serveFile(req, index_path);
  } else {
    if(Deno.statSync(path).isFile) return serveFile(req, path);
    return new Response('404 Not Found', { status: 404 });
  }

  const is_root = url.pathname == '/';

  let items: TreeItem[] = [];

  for(const entry of Deno.readDirSync(path)) {
    if(!args.hidden && entry.name.startsWith('.')) continue;

    items.push({
      is_directory: entry.isDirectory,
      name: entry.name,
      path: join(url.pathname, entry.name),
      href: `${join(url.pathname, entry.name)}${ entry.isDirectory ? '/' : ''}`
    })
  }

  items = items.sort((a, b) => a.is_directory == b.is_directory ? 0 : a.is_directory ? -1 : 1);

  const directories = items.filter(item => item.is_directory);
  const files = items.filter(item => !item.is_directory);

  const content = `
    <div class="topbar">
      <div class="title">http-server</div>
      <a href="/" style="color: white" class="title">${url.protocol}//${url.host}</a>
    </div>
    <div style="padding: 2rem 4rem; margin-top: 80px;">
      <div class="navbar">
        <div>
          ${ is_root ? '' : `<a class="button" href='${url.pathname.replace(/\/[\.\w]*\/?$/, '') || '/'}'>Up</a>` }
        </div>
        <div>Current path: <span class="path">${ url.pathname }</span></div>
        <div class="flex">
          <a class="button" href="/http-server-mode-switch?mode=grid">Grid</a>
          <a class="button" href="/http-server-mode-switch?mode=list">List</a>
        </div>
      </div>
      <div class="content">
        ${items.length ? `
          ${directories.length ? `
            ${renderer.render(directories)}
          ` : ''}
          ${files.length ? `
            ${renderer.render(files)}
          ` : ''}
        ` : '<h3 style="padding: 1rem 2rem;">This directory is empty</h3>'}
      </div>
    </div>
  `;

  const html = html_base
    .replace(/\<Title\s*\/\>/, `http-server:${url.pathname}`)
    .replace(/\<Content\s*\/\>/, content)
    .replace(/\<style\s+id="root-styles"\s*\>/, `<style>${css_base}`);

  return new Response(html, {
    headers: new Headers({
      'Content-Type': 'text/html'
    })
  });
})