2022年12月16日 星期五

用 dart 語言寫簡易帶上傳功能的 http 檔案伺服器

 import 'dart:convert';
import 'dart:io';
ContentType jsType    = ContentType('application','javascript',charset:'UTF-8');
ContentType textType  = ContentType('text','plain',charset:'UTF-8');
ContentType htmlType  = ContentType('text','html',charset: 'UTF-8');
List<int> disposition = 'Content-Disposition:'.codeUnits;
List<int> multipart   = 'Content-Type:'.codeUnits;
List<int> u8x2crlf    = '\r\n\r\n'.codeUnits;
List<int> u8crlf__    = '\r\n--'.codeUnits;
const styleTag = '''<style>
  body {background:black; color:green; font-size:16px}
  a {margin-left:32px}
  .buttonLike {background:black; color:dodgerblue; cursor:pointer}
  .buttonLike:hover {background:gray; color:white}
</style>''';// css
const buttonJS = '''<input hidden id="pickOne" type="file">
<label for="pickOne" class="buttonLike" id="choose">上傳檔案 ?</label>
<button id="uploadButton" onclick="uploadFile()" disabled="true"></button><br>
<script type="application/javascript">
  pickOne.addEventListener("change", (e)=> {
    choose.innerHTML = e.target.files[0].name;
    uploadButton.disabled = false;
    uploadButton.innerHTML= "開始上傳";
  });
  const uploadFile = () => {
    if (pickOne.files.length > 0) {
      pickOne.disabled = true;
      uploadButton.disabled = true;
      uploadButton.textContent = "上傳中, 請稍候...";
      const formData = new FormData();
      formData.append("appAPK", pickOne.files[0]);
      fetch("/postTMP", {method: "POST", body: formData})
        .then((blob)   => console.log("上傳成功."))
        .catch((error) => console.log("上傳失敗!"))
        .finally(()    => location.reload());
    }
  }
</script>''';
const headTag = '<head><meta charset="UTF-8">$styleTag</head>';
const endbodyhtml = '</body></html>';
const htmlHere='<!DOCTYPE html><html>$headTag<body>It works.</body></html>';

int indexof(List<int> data, List<int> subList) {
  int matchlength = subList.length;
  int size = data.length;
  int slider = 0;
  while (slider < size) {
    int endPos = slider + matchlength;
    if (endPos > size) return -1;
    int position = slider;
    for (int i = 0; i < matchlength; i++) {
      if (data[position] != subList[i]) break;
      position ++;
    }
    if (endPos == position) return slider;
    slider ++;
  }
  return -1;
}
String formParse (List<int> data, String key) {
  List<String> args = utf8.decode(data).trim().split(';');
  for (final pair in args) {
    if (! pair.contains(key) || ! pair.contains('=')) continue;
    int start = pair.indexOf('"');
    int last  = pair.lastIndexOf('"');
    if (start >= 0 && last > 1) return pair.substring(start + 1, last);
    start = pair.indexOf("'");
    last  = pair.lastIndexOf("'");
    if (start >= 0 && last > 1) return pair.substring(start + 1, last);
    return pair.split('=').last;    
  }
  return '';
}
void postListen (HttpRequest req, String path) {
  Directory uploadDir = Directory(path);
  String? tag = req.headers.contentType?.parameters['boundary'];
  if (tag == null) req.response.close();
  else {
    List<int> boundary = tag.codeUnits;
    IOSink? sink = null;
    req.listen (
      (List<int> data) {
        int start = indexof(data, boundary);
        if (start < 0) sink ?. add(data);
        else {        
          start += 2 + boundary.length;
          List<int> form = data.sublist(start);
          int offset = indexof(form, disposition);
          if (offset >= 0) {
            offset = indexof(form, u8x2crlf);
            if (offset >= 0) {
              start += offset + u8x2crlf.length;
              int eos = indexof(form, multipart);
              if (eos > 0) {
                if (! uploadDir.existsSync()) {
                  uploadDir.createSync(recursive: true);
                }
                String name = formParse(form.sublist(0, eos), 'filename');
                if (name.length > 0) {
                  sink = File('${uploadDir.path}/$name').openWrite();
                }
              }
            }
          }
          final startData = data.sublist(start);
          int size = indexof(startData, boundary);
          if (size >= 0) {
            if (indexof(startData, u8crlf__) > 0) size -= 4;
            sink ?. add(data.sublist(start, start + size));
          } else {
            if (offset > 0) {
              sink ?. add(startData);
            } else  {
              size = indexof(data, boundary);
              if (indexof(data, u8crlf__) > 0) size -= 4;
              sink ?. add(data.sublist(0, size));
            }
          }
        }
      },
      onError: (_) => req.response.close(),
      onDone : ( ) {
        req.response .. statusCode = 200 .. write('OK') .. close();
        sink ?. close();
      }
    );
  }
}
void getPipe(String pathname, HttpResponse reply) {
  String fname = (pathname == '/index') ? './index.html' : '.$pathname';  
  final name = Uri.decodeFull(fname);
  File file2Reply = File(name);
  reply .. statusCode = 200 .. headers.contentType = htmlType;
  if (! file2Reply.existsSync() && ! name.endsWith('/')) {
    if (name == './hello' || name == './index.html') reply.write(htmlHere);
    else  reply .. statusCode = 404 ..
      write('<!DOCTYPE html><html><body>404 Not found!</body></html>');    
    reply.close();
    return;
  }
  if (name.endsWith('/')) {
    final dir = Directory(name);
    reply.write('<!DOCTYPE html><html>$headTag<body>$buttonJS 檔案清單:<br>');
    if (dir.existsSync()) dir.listSync() ..  
      sort((a, b) {
        if (a is Directory && b is File) return  1;
        if (a is File && b is Directory) return -1;
        return a.path.toLowerCase().compareTo(b.path.toLowerCase());
      }) ..
      forEach((FileSystemEntity e) {
        final url = Uri.encodeFull(e.path.replaceFirst('.', ''));
        final show = e.path.replaceFirst(name , '');
        if (e is File)      reply.write('<a href="$url">  檔案 $show</a><br>');
        if (e is Directory) reply.write('<a href="$url/"> 目錄 $show</a><br>');
      });
    reply .. write(endbodyhtml) .. close();
    return;
  }      
  reply ..
    headers.contentLength = file2Reply.lengthSync() ..
    headers.contentType   = name.endsWith('.js') ? jsType  :
      RegExp(r'(\.htm|\.html)$').hasMatch(name) ? htmlType :
      RegExp(r'(\.txt|\.dart|\.pem|\.c|\.h)$').hasMatch(name) ? textType :
      name.endsWith('.css') ? ContentType('text',  'css')  :
      name.endsWith('.jpg') ? ContentType('image', 'jpg')  :
      name.endsWith('.png') ? ContentType('image', 'png')  :
      ContentType('application', 'octet-stream');
  file2Reply.openRead().pipe(reply);
}
void main(List<String> args) {
  final ip = InternetAddress.anyIPv4;
  int   port = args.length > 0 ? int.parse(args[0]) : 8080;
  print('http://${ip.host}:$port');
  HttpServer.bind(ip, port).then((HttpServer server){
    server.listen((HttpRequest req) {       
      String pathname = req.uri.path;   
      if (req.method == 'GET') getPipe(pathname, req.response);
      else if(pathname =='/postTMP') postListen(req, './上傳暫存區');
      else req.response.close();
    });
    ProcessSignal.sigint.watch().listen((ProcessSignal signal) {// Ctrl + C
      server.close();
      exit(1);
    });
  });  
}

沒有留言:

張貼留言

使用 pcie 轉接器連接 nvme SSD

之前 AM4 主機板使用 pcie ssd, 但主機板故障了沒辦法上網, 只好翻出以前買的 FM2 舊主機板, 想辦法讓老主機復活, 但舊主機板沒有 nvme 的界面, 因此上網買了 pcie 轉接器用來連接 nvme ssd, 遺憾的是 grub2 bootloader 無法識...