aardio 文档

aardio 范例: qrfs - 扫码快传 v2.3

//扫码传文件
import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="qrfs - 扫码快传 v2.3";right=807;bottom=465;bgcolor=0xFFFFFF;border="none";max=false)
winform.add(
bk={cls="bk";left=-2;top=-5;right=810;bottom=29;bgcolor=15790320;dl=1;dr=1;dt=1;forecolor=12639424;linearGradient=0;z=10};
bkplus={cls="bkplus";text="qrfs - 扫码快传  v2.3";left=35;top=3;right=220;bottom=25;align="left";color=5921370;dl=1;dt=1;z=11};
btnOpen={cls="plus";text='\uF115';left=444;top=50;right=479;bottom=75;dr=1;dt=1;font=LOGFONT(h=-16;name='FontAwesome');notify=1;z=6};
btnOpenUpload={cls="plus";text="打开上传目录";left=161;top=86;right=302;bottom=115;dl=1;dt=1;font=LOGFONT(h=-15);iconStyle={align="left";font=LOGFONT(h=-15;name='FontAwesome');padding={left=8;top=2}};iconText='\uF115';notify=1;textPadding={left=20};z=12};
btnStart={cls="plus";text="重启服务";left=655;top=47;right=755;bottom=76;bgcolor=14935259;dr=1;dt=1;font=LOGFONT(h=-15);iconStyle={align="left";font=LOGFONT(h=-15;name='FontAwesome');padding={left=8;top=2}};iconText='\uF233';notify=1;textPadding={left=20};z=5};
editDocumentRoot={cls="plus";left=131;top=49;right=430;bottom=73;align="right";border={bottom=1;color=-8355712};dl=1;dr=1;dt=1;editable=1;font=LOGFONT(h=-16);z=8};
editPassword={cls="plus";left=441;top=84;right=632;bottom=108;align="left";border={bottom=1;color=-8355712};dr=1;dt=1;editable=1;font=LOGFONT(h=-16);z=14};
editPort={cls="plus";left=550;top=49;right=628;bottom=73;align="left";border={bottom=1;color=-8355712};dr=1;dt=1;editable=1;font=LOGFONT(h=-16);z=9};
plus={cls="plus";text="访问密码:";left=332;top=90;right=433;bottom=114;align="right";dl=1;dr=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=13};
plus2={cls="plus";left=38;top=128;right=472;bottom=422;align="left";border={color=-16744448;radius=8;width=1};db=1;dl=1;dr=1;dt=1;font=LOGFONT(h=-14);textPadding={left=16};valign="top";z=1};
qr={cls="plus";left=499;top=132;right=760;bottom=418;db=1;dr=1;dt=1;repeat="scale";z=7};
radioQrClipboard={cls="radiobutton";text="共享剪贴板";left=685;top=435;right=772;bottom=454;bgcolor=0xFFFFFF;db=1;dr=1;z=18};
radioQrRootDir={cls="radiobutton";text="根目录";left=502;top=435;right=589;bottom=454;bgcolor=0xFFFFFF;db=1;dr=1;z=16};
radioQrUploadDir={cls="radiobutton";text="上传目录";left=593;top=435;right=680;bottom=454;bgcolor=0xFFFFFF;db=1;dr=1;z=17};
static={cls="plus";text="端口:";left=484;top=52;right=547;bottom=76;align="right";dr=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=3};
static2={cls="plus";text="网站根目录:";left=15;top=52;right=129;bottom=76;align="right";dl=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=4};
syslink={cls="syslink";text='<a href="https://github.com/aardio/qrfs">开源项目</a>';left=43;top=437;right=176;bottom=457;bgcolor=0xFFFFFF;db=1;dl=1;z=15};
txtMessage={cls="richedit";left=42;top=132;right=469;bottom=418;autohscroll=false;db=1;dl=1;dr=1;dt=1;link=1;multiline=1;vscroll=1;z=2}
)
/*}}*/

import fsys.update.simpleMain;
if( fsys.update.simpleMain(
    "qrfs - 扫码快传",
    "http://d.aardio.com/qrfs/update/",
    io.appData("/aardio/std/qrfs/app/update"),
    function(version,description,status){})){
    return 0;   
}

import fsys.config;
config = fsys.config(io.appData("aardio/std/qrfs")); 

import sessionHandler.default;
sessionHandler.default.root = io.appData("aardio/std/qrfs/session")

if( io.exist(config.winform.txtMessage) ){
    winform.txtMessage.text = config.winform.txtMessage;
}
else {
    winform.txtMessage.text = io.getSpecial(0x5/*_CSIDL_MYDOCUMENTS*/)
}

if(config.winform.qrDir=="upload"){
    winform.radioQrUploadDir.checked = true; 
}
elseif(config.winform.qrDir=="clipboard"){
    winform.radioQrClipboard.checked = true;
}
else {
    winform.radioQrRootDir.checked = true;
} 

import web.socket.server;
var wsrv = web.socket.server();

var srvHttp = wsrv.httpServer;
/*
wsrv.httpServer 是实现单线程异步 HTTP 服务端的 wsock.tcp.asynHttpServer 对象。
浏览器组件发起异步 HTTP 请求支持 wsock.tcp.asynHttpServer。请不要用 inet.http 等
阻塞请求同一线程创建的 asynHttpServer,这会导致 asynHttpServer 没有机会响应请求而导致死锁,
如果确有这样的需求,可创建线程发起请求,或改用基于多线程的 wsock.tcp.simpleHttpServer。
*/
srvHttp.documentRoot = winform.txtMessage.text;
srvHttp.userToken = string.random(18);
winform.editPassword.text = srvHttp.userToken;


import fsys.info; 
var cacheSysIcons = {}
var getSysIconIndex = function(path){ 
    var sfi;
    sfi = fsys.info.get(path, 0x100/*_SHGFI_ICON*/ | 0x4000/*_SHGFI_SYSICONINDEX*/|0x10/*_SHGFI_USEFILEATTRIBUTES*/, 
        ..fsys.isDir(path)?0x10/*_FILE_ATTRIBUTE_DIRECTORY*/?0x80/*_FILE_ATTRIBUTE_NORMAL*/);

    if( !(sfi.returnValue ) ) {
        return; 
    }

    if(!cacheSysIcons[sfi.iIcon]){
        var dataUrl;
        var bmp = ..gdip.bitmap(sfi.hIcon,1/*_IMAGE_ICON*/);
        if(bmp){
            cacheSysIcons[sfi.iIcon] = bmp.saveToBuffer(".png"); 
            bmp.dispose();
        }
    }
    if(sfi.hIcon)::DestroyIcon(sfi.hIcon);  
    return sfi.iIcon;
}

import win.clip;
var cacheClientIps = {}
srvHttp.run( 
    function(response,request,session){ 

        //添加禁止缓存的HTTP头
        response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
        response.headers["Pragma"] = "no-cache";
        response.headers["Expires"] = "0";

        var token = request.get["t"] : session["token"];
        if( #srvHttp.userToken && (token != srvHttp.userToken) ){
            winform.txtMessage.printf("客户端:%s 连接被拒绝",request.remoteAddr); 
            response.errorStatus(401)
            return;
        }
        session["token"] = token;

        if(!cacheClientIps[request.remoteAddr]){
            winform.txtMessage.printf("客户端:%s 已连接",request.remoteAddr);   
            cacheClientIps[request.remoteAddr] = true;
        }

        response.headers["Access-Control-Allow-Origin"] = "*";
        response.headers["Access-Control-Allow-Headers"] = "*"

        if(request.path=="/main.aardio" && request.get["icon"]){
            var iconIdx = tonumber(request.get["icon"]);
            if(iconIdx!==null){
                if(cacheSysIcons[iconIdx]){
                    response.contentType = "image/png";
                    response.write(cacheSysIcons[iconIdx])
                    return;
                }
            }
            response.errorStatus(404);
            return;
        }

        if(request.path=="/upload/main.aardio"){
            if(request.method=="DELETE"){
                var path = request.postData();
                if(path && string.startsWith(path,"/upload/")){
                    path = ..io.joinpath(srvHttp.documentRoot,path)

                    if(io.exist(path)){
                        io.remove(path);
                        winform.txtMessage.print("已删除:" + path);
                        response.close();
                        return; 
                    }
                }

                response.errorStatus(404);
                return;
            }

            fileData = request.postFileData()
            if(fileData){
                io.createDir(..io.joinpath(srvHttp.documentRoot,"upload"))
                winform.txtMessage.print(..io.joinpath(srvHttp.documentRoot,"upload"))

                var fileName = ..io.joinpath(srvHttp.documentRoot,"upload",fileData.filepond.filename) 
                var ok,err = fileData.filepond.save(fileName); 
                if(!ok){ response.error(err); }

                winform.txtMessage.text = 'HTTP 服务端已启动: \n'; 
                winform.txtMessage.print( srvHttp.getUrl(,true) + "/?t=" + srvHttp.userToken  );

                winform.txtMessage.print( "" );     
                winform.txtMessage.print( "上传成功:" + fileName );   

                response.contentType = "text/plain";
                response.write("/upload/",fileData.filepond.filename)
                return response.close() 
            }       
        }

        if(!fsys.isDir(request.path) && request.path!="/<clipboard>" ) {
            if( ..io.exist(request.path) 
                && (!_STUDIO_INVOKED || request.path!="/main.aardio") )
                return response.loadcode(request.path)
            else {
                request.path = fsys.getParentDir(request.path)
            }
        } 

        response.write(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<title>qrfs - 扫码快传  v2.3</title>
<link href="https://lib.baomitu.com/filepond/4.28.2/filepond.min.css" rel="stylesheet">
<script src="https://lib.baomitu.com/filepond/4.28.2/filepond.min.js"></script> 
<script>

</script>

<style>

html{
    margin: 0;
    padding: 0;
}

body {
    padding-top: 60px;
    height: 100vh;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}   

li{ list-style-type:none; }

.filepond-container {
    position: fixed;
    top: 0;
    right: 0;
    width: 100%;
    height: min-content;
    z-index: 1000; 
}

.filepond--root {
    min-height: 30px !important; 
    margin-bottom: 0;
}

.filepond--drop-label {
    height: 60px !important; 
}

.filepond--panel-root { 
    min-height:  30px !important;  
    border: 2px dashed #99aab5;
}

.filepond--drop-label {
    color: #718096;
    min-height:  30px !important; 
}

.filepond--item {
    min-height:  30px !important;
    margin: 0 !important;
}

.filepond--file{
    min-width: 100%;
    padding-right: 80px;
}

.filepond--file-info {
    min-height: auto !important;
    margin: 0 !important;
}

.filepond--label-action {
    color: #3498db;
}

.filepond--file-status {
    margin: 0 !important;
    min-height: auto !important;
}

.filepond--credits {
    display: none;
}

.filepond--file-wrapper {
    height: 100% !important;
    display: flex;
    align-items: center;
}

.filepond--processing-complete-indicator {
    margin: 0 !important;
}

ul{
  margin-top:0px;
}

p, li {
    line-height: 1.7;
    white-space: nowrap;
    text-overflow: ellipsis; 
    overflow: hidden; 
    font-size: 1.1em;
}

li img{
    vertical-align: middle;
    width: 1.7em !important;
    height: 1.7em !important;
    padding-right: 5px;
}


a {
    color: #0366d6; 
    text-decoration: none;
    font-size: 1.1em !important;
}

a:hover {
    text-decoration: underline;
    color: #0056b3;
}

a.absent {
    color: #cc0000; 
}

a.anchor {
    display: block;
    padding-left: 30px;
    margin-left: -30px;
    cursor: pointer;
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0; 
}

h1, h2, h3, h4, h5, h6 {
    line-height: 1.5em;
    margin: 15px 0 10px;
    padding: 0;
    font-weight: bold;
    -webkit-font-smoothing: antialiased;
    cursor: text;
    position: relative; 
} 

h2 {
    font-size: 1.3em;
    padding-bottom: 6px;
    border-bottom: 1px solid #DEE3E8;
    color: #333;
    margin-top:5px;
    margin-bottom: 5px;
    vertical-align: middle;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.back-btn {
    padding: 8px 15px;  
    cursor: pointer;
    font-size: 0.8em;
    margin-left: 10px;
    font-weight: 400;
} 

.clipboard-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: 0 10px;
    max-height: calc(100vh - 20px);
}

.text-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    margin-bottom: 15px;
    position: relative;
    max-height: 100%;
}

.clipboard-content {
    width: 100%;
    height: 100%;
    padding: 15px;
    border: 1px solid #ddd;
    border-radius: 8px;
    font-size: 1.1em;
    background-color: #fff;
    resize: none;
    box-sizing: border-box;
    line-height: 1.5;
}

.clipboard-content:focus {
    outline: none;
    border-color: #2196F3;
    height: 100% !important;
    position: relative !important;
    box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}

.button-group {
    display: flex;
    gap: 12px;
    margin-bottom: 16px;
    position: sticky;
    bottom: 0;
    background-color: #fff;
    padding: 10px 0;
    z-index: 100;
}

button {
    flex: 1;
    padding: 16px 0;
    border: none;
    border-radius: 8px;
    font-size: 1.2em;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s;
    color: white;
}

.copy-btn {
    background-color: #2196F3;
}

button:active {
    opacity: 0.8;
}

</style>
</head>
<body> 

<script crossorigin="anonymous"> 
websocket = new WebSocket("`+wsrv.getUrl("ws",true)+`");

var clipboardContent;
websocket.onmessage = function(evt) {
    if(evt.data=="reload"){
        window.location.pathname = "/";
        window.location.reload(true)
    }
    else{ 
        if(clipboardContent){
            clipboardContent.value = evt.data;
        }
    }
};   
</script>
`)


if(request.path!="/<clipboard>/"){
    response.write(`
<div class="filepond-container">
<input type="file" class="filepond" name="filepond" multiple>
</div>
<script crossorigin="anonymous"> 
if(document.body.style.order === undefined){
    alert("浏览器版本过低,请使用Chrome或IE11以上版本浏览器打开此页面!")
}

var inputElement = document.querySelector('input[type="file"]');
FilePond.create(inputElement,{
    onprocessfiles: function(){
        `+ (..io.exist("/upload/") ? "" : " if (window.location.pathname === '/') window.location.reload(true);") +`

        if (window.location.pathname === '/upload/'){
            window.location.reload(true)
        } 
    }

});
FilePond.setOptions({
    server: '/upload/?t=` + srvHttp.userToken + `',
    labelIdle: '拖拽 / <span class="filepond--label-action">选择上传文件</span>',
    labelFileProcessing: '上传中...',
    labelFileProcessingComplete: '上传成功',
    labelTapToUndo: '点击撤消',
    labelTapToCancel: '点击取消',
    labelTapToRetry: '点击重试',
    labelFileLoadError: '上传时遇到错误'
});

</script> `)
}

        if(request.path=="/<clipboard>/"){
            response.write(`<h2>/共享剪贴板/ 📋 <a class="back-btn" onclick="javascript:location.href='/'">⬑父目录</a></h2>`)
        }
        elseif(request.path=="/upload/"){
            response.write(`<h2>当前目录:/upload/ 📤 <a class="back-btn" onclick="javascript:location.href='/'">⬑父目录</a></h2><ul>`)
        }
        elseif(#request.path>2){
            if(string.find(request.path,"^/[^/]+/$")){
                response.write(`<h2>当前目录:`
                    ,request.path,` <a class="back-btn" onclick="javascript:location.href='/'">⬑父目录</a></h2><ul>`)
            }
            else{
                response.write(`<h2>当前目录:`
                    ,request.path,`</h2> <a class="back-btn" onclick="history.back()">⬑父目录</a> <ul>`)
            }
        }
        else {
            response.write(`<h2>当前目录:`
                    ,request.path,`</h2><ul>`)
        } 

        if(request.path=="/" && ..io.exist("/upload/")){
            response.write('<li><img src="/?icon='++getSysIconIndex("/upload/")+'"><a href="/upload?t=' + srvHttp.userToken + '">上传目录 📤</a><br>\r\n');   
        }

        if(request.path=="/<clipboard>/"){

            response.write(`

    <div class="clipboard-container">
        <div class="text-container">
            <textarea id="clipboardContent" class="clipboard-content" >`+(win.clip.read():"")+`</textarea>
        </div>

        <div class="button-group">
            <button id="copyBtn" class="copy-btn">复制</button> 
        </div> 
    </div>  

    <script>
    document.body.style.paddingTop = 0;

    clipboardContent = document.getElementById('clipboardContent');

    clipboardContent.addEventListener('input', (e) => {
          websocket.send(e.target.value);
    });

    const copyBtn = document.getElementById('copyBtn'); 

    copyBtn.addEventListener('click', () => { 
        clipboardContent.select();
        document.execCommand('copy'); 

        const originalText = copyBtn.textContent;
        copyBtn.textContent = '已复制!';
        setTimeout(() => {
            copyBtn.textContent = originalText;
        }, 1500);
    });

    clipboardContent.addEventListener('focus', () => {
        setTimeout(() => {
            window.scrollTo(0, 0);
        }, 100);
    });

    </script></body></html>
`)
            return;
        }
        elseif(request.path=="/") {
            response.write('<li><img src="/?icon='++getSysIconIndex("*.txt")+'"><a href="/%3Cclipboard%3E/?t=' + srvHttp.userToken + '">共享剪贴板 📋</a><br>\r\n'); 
        }

        var file,dir = fsys.list(request.path,,"*.*");
        for(i=1;#dir;1){
            if(dir[i]==="upload" && request.path=="/") continue;

            var iconIdex = getSysIconIndex(dir[dir[ i ]])
            response.write('<li><img src="/?icon='++(iconIdex)+'"><a href="'
                ,inet.url.append(request.path, inet.url.encode(dir[ i ]) )
                ,'?t=' + srvHttp.userToken + '">',dir[ i ],'</a><br>\r\n');
        }

        for(i=1;#file;1){
            var iconIdex = getSysIconIndex(file[file[ i ]])
            response.write('<li><img src="/?icon='++(iconIdex)+'"><a href="'
                    ,inet.url.append(request.path,inet.url.encode(file[ i ]) )
                    ,'?t=' + srvHttp.userToken + '">',file[ i ],'</a><br>\r\n');
        }

        response.write("</ul></body></html>")
    }   
);

import qrencode.bitmap;
var serverInfo = function(){
    var ip,port = srvHttp.getLocalIp();
    winform.editPort.text = port;
    winform.editDocumentRoot.text = io.fullpath(srvHttp.documentRoot)

    var home = "/";
    if(winform.radioQrUploadDir.checked){
        home = "/upload/";
    }
    elseif(winform.radioQrClipboard.checked){
        home = "/%3Cclipboard%3E/";
    }

    var url = srvHttp.getUrl(home,true);
    if(#srvHttp.userToken){
        url = url ++ "?t=" + srvHttp.userToken;
    } 

    winform.txtMessage.text = 'HTTP 服务端已启动: \n'; 
    winform.txtMessage.print(  url );

    var qrBmp = qrencode.bitmap( url );
    winform.qr.setBackground(qrBmp.copyBitmap(winform.qr.width)); 

    winform.txtMessage.print( 
        "
手机无线连接电脑局域网。
扫码打开网页,可上传下载文件、共享电脑剪贴板。
拖动文件或目录到窗口网页自动刷新。

aardio 实现的开源单线程异步 HTTP 服务端,体积仅数十 KB。
支持高速上传下载、断点续传、304 缓存、分块传输、Keep Alive。
支持 WebSocket / HTTP 双服务端(共享端口)。
可运行 aardio 开发的网站。
"
    );  
}
serverInfo();

winform.btnStart.oncommand = function(id,event){
    winform.txtMessage.text = "";
    winform.btnStart.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250'}
    win.delay(500);

    var port = tonumber(winform.editPort.text);
    srvHttp.documentRoot = fsys.isDir(winform.editDocumentRoot.text) ? winform.editDocumentRoot.text;
    srvHttp.userToken = winform.editPassword.text;
    srvHttp.start("0.0.0.0",port);
    serverInfo();

    winform.btnStart.disabledText = null;
}

import win.ui.menu;
winform.txtMessage.enablePopMenu();
winform.txtMessage.onHyperlink = function(message,href){
    if( message = 0x202/*_WM_LBUTTONUP*/ ) {
        import process;
        process.openUrl(href);
    }
}

winform.onDropFiles = function(files){
    var path = files[1]
    if(!fsys.isDir(path)){
        path = fsys.getParentDir(path)
    }

    winform.editDocumentRoot.text = path;
    srvHttp.documentRoot = path;
    config.winform.txtMessage = path;
    config.winform.save();

    wsrv.publish("reload");
}

import fsys.dlg.dir;
winform.btnOpen.oncommand = function(id,event){
    var dir = fsys.dlg.dir(winform.editDocumentRoot.text,winform)
    if(dir){
        winform.editDocumentRoot.text = dir;
        srvHttp.documentRoot = dir;

        config.winform.txtMessage = dir;
        config.winform.save();
        wsrv.publish("reload");
    }
}

import process;
winform.btnOpenUpload.oncommand = function(id,event){
    var path = io.joinpath(winform.editDocumentRoot.text,"upload")
    if(io.createDir(path)){
        process.explore(path)
    }
}

import win.clip;
import win.clip.viewer;
var clipViewer = win.clip.viewer(winform);
clipViewer.onDrawClipboard=function(){
    var str = win.clip.read();  
    if(str!=wsrv.lastReceivedClipboardData ){
        wsrv.publish(#str?str:"") 
        wsrv.lastReceivedClipboardData = null;   
    }
}

wsrv.onMessage = function(hSocket,msg){
    wsrv.lastReceivedClipboardData = msg.data;
    win.clip.write(msg.data);
}

var updateHomeDir = function(){
    var home = "/";
    if(winform.radioQrUploadDir.checked){
        home = "/upload/";
    }
    elseif(winform.radioQrClipboard.checked){
        home = "/%3Cclipboard%3E/";
    }

    var url = srvHttp.getUrl(home,true);
    if(#srvHttp.userToken){
        url = url ++ "?t=" + srvHttp.userToken;
    }  

    var qrBmp = qrencode.bitmap( url );
    winform.qr.setBackground(qrBmp.copyBitmap(winform.qr.width));   
}

winform.radioQrRootDir.oncommand = function(id,event){
    config.winform.qrDir = "root";
    updateHomeDir();
}

winform.radioQrUploadDir.oncommand = function(id,event){
    config.winform.qrDir = "upload";
    updateHomeDir();    
}

winform.radioQrClipboard.oncommand = function(id,event){
    config.winform.qrDir = "clipboard"; 
    updateHomeDir();
}

import win.ui.simpleWindow2;
win.ui.simpleWindow2(winform);
winform.show();  

winform.btnStart.skin( {
    background={
        default=0x668FB2B0;
        hover=0xFF928BB3;
        disabled=0xFFCCCCCC; 
    }
})

winform.btnOpen.skin( {
    background={
        default=0;
        hover=0xFF928BB3;
        disabled=0xFFCCCCCC; 
    }
})

winform.btnOpenUpload.skin( {
    background={
        default=0;
        hover=0xFF928BB3;
        disabled=0xFFCCCCCC; 
    }
})

win.loopMessage();
Markdown 格式