UWInfo Blog
發表新文章
[Join] | [忘記密碼] | [Login]
搜尋

搜尋意見
文章分類-#Author#
[所有文章分類]
所有文章分類
  • ASP.NET (48)
  • ASP.NET2.0 (15)
  • ASP.NET4.0 (34)
  • JavaScript (49)
  • jQuery (26)
  • FireFox (4)
  • UW系統設定 (3)
  • SQL (39)
  • SQL 2008 (25)
  • mirror (4)
  • SVN (4)
  • IE (9)
  • IIS (20)
  • IIS6 (1)
  • 閒聊 (7)
  • W3C (6)
  • 作業系統 (9)
  • C# (24)
  • CSS (12)
  • FileServer (1)
  • HTML 5 (11)
  • CKEditor (3)
  • UW.dll (13)
  • Visual Studio (16)
  • Browser (8)
  • SEO (1)
  • Google Apps (3)
  • 網站輔助系統 (4)
  • DNS (5)
  • SMTP (4)
  • 網管 (11)
  • 社群API (3)
  • SSL (4)
  • App_Inventor (1)
  • URLRewrite (2)
  • 開發工具 (6)
  • JSON (1)
  • Excel2007 (1)
  • 試題 (3)
  • LINQ (1)
  • bootstrap (0)
  • Vue (3)
  • IIS7 (3)
  • foodpanda (2)
  • 編碼 (2)
  • 資安 (3)
  • Sourcetree (1)
  • MAUI (1)
  • CMD (1)
  • my sql (1)
最新回應
  • Newtonsoft.Json.JsonConvert.DeserializeObject 失敗的情況
    test...more
  • dotnet ef dbcontext scaffold
    ...more
  • [ASP.NET] 利用 aspnet_regiis 加密 web.config
    ...more
  • IIS ARR (reverse proxy) 服務安裝
    ...more
  • [錯誤訊息] 請加入 ScriptResourceMapping 命名的 jquery (區分大小寫)
    ...more
  • 用 Javascript 跨網頁讀取 cookie (Cookie cross page, path of cookie)
    ...more
  • 線上客服 - MSN
    本人信箱被盜用以致資料外洩,是否可以請貴平台予以協助刪除該信箱之使用謝謝囉...more
  • 插入文字到游標或選取處
    aaaaa...more
  • IIS 配合 AD (Active Directory) 認証, 使用 .Net 6.0
    太感謝你了~~~你救了我被windows 認證卡了好幾天QQ...more
  • PostgreSQL 的 monitor trigger
    FOR EACH ROW 可能要改為 FOR EACH STATEMENT ...more
標籤
  • for
  • a generic
  • RSA
  • Find
  • image
  • deferred
  • asp.net
  • DB
  • 預覽
  • 超出最大長度
  • 超出
  • 起始不標示為安全的a
  • 資料庫備份
  • 資安
  • 試,
  • 試題
  • 德蒄
  • Chrome[t]
  • iis
  • Line
  • line pay介接
  • 手機版網頁
  • server
  • IP
  • mssql 備份
  • inertSQL
  • TLS1
  • web test
  • 98
  • http 錯èª
  • AD
  • separatedi
  • 鎖定
  • 檔案
  • vs
  • Server Err
  • 出單機
  • window
  • [t]
  • async
  • 220
  • bugzilla
  • load
  • 1021211211
  • 70
  • 中文
  • face
  • C
  • 288
  • ddd
頁數 1 / 5 下一頁
搜尋 for 結果:
AWS SES 建立 SNS 通知
在 powershell 執行以下的指令


# List of SES identities
$identities = @(
    "wztech.com.tw",
    "s3.com.tw",
    "jdcard.com.tw",
    "mskcable.com",
    "uwinfo.com.tw",
    "bike.idv.tw",
    "richwave.com.tw",
    "ctcn.com.tw",
    "jcard.com.tw",
    "bike@bike.idv.tw",
    "ee@ier.org.tw"
)

foreach ($identity in $identities) {
    # Convert identity to a valid topic name by replacing '@' and '.' with '_'
    $safeIdentity = $identity -replace "@", "_" -replace "\.", "_"
    $topicName = "SES_NOTIFY_$safeIdentity"
    $endpoint = "https://working.uwinfo.com.tw/aws/api/sns/receive?topic=$topicName"

    # Validate topic name format
    if ($topicName -notmatch '^[a-zA-Z0-9_\-\$]+$') {
        Write-Host "❌ Invalid topic name: $topicName"
        continue
    }

    # 1. Create SNS topic
    $topicArn = aws sns create-topic `
        --name $topicName `
        --query 'TopicArn' `
        --output text

    Write-Host "✔ Created topic: $topicArn"

    # 2. Subscribe webhook
    aws sns subscribe `
        --topic-arn $topicArn `
        --protocol https `
        --notification-endpoint $endpoint

    Write-Host "✔ Subscribed webhook: $endpoint"

    # 3. Link SES notifications
    foreach ($type in @("Delivery", "Bounce", "Complaint")) {
        aws ses set-identity-notification-topic `
            --identity $identity `
            --notification-type $type `
            --sns-topic $topicArn
        Write-Host "✔ $type linked to $topicName"
    }

    Write-Host "✅ Setup complete for $identity\n"
}

Write-Host "🎉 All identities processed."



收到的 log 會放在這裡: E:\WebBackup\195\ASP.NET Project\working\Data\Log\sns

 
 
More...
Bike, 2025/4/16 上午 10:40:39
AWS SES 建立 SNS 通知
在 powershell 執行以下的指令

# List of SES identities
$identities = @(
    "wztech.com.tw",
    "s3.com.tw",
    "jdcard.com.tw",
    "mskcable.com",
    "uwinfo.com.tw",
    "bike.idv.tw",
    "richwave.com.tw",
    "ctcn.com.tw",
    "jcard.com.tw",
    "bike@bike.idv.tw",
    "ee@ier.org.tw"
)

foreach ($identity in $identities) {
    # Convert identity to a valid topic name by replacing '@' and '.' with '_'
    $safeIdentity = $identity -replace "@", "_" -replace "\.", "_"
    $topicName = "SES_NOTIFY_$safeIdentity"
    $endpoint = "https://working.uwinfo.com.tw/aws/api/sns/receive?topic=$topicName"

    # Validate topic name format
    if ($topicName -notmatch '^[a-zA-Z0-9_\-\$]+$') {
        Write-Host "❌ Invalid topic name: $topicName"
        continue
    }

    # 1. Create SNS topic
    $topicArn = aws sns create-topic `
        --name $topicName `
        --query 'TopicArn' `
        --output text

    Write-Host "✔ Created topic: $topicArn"

    # 2. Subscribe webhook
    aws sns subscribe `
        --topic-arn $topicArn `
        --protocol https `
        --notification-endpoint $endpoint

    Write-Host "✔ Subscribed webhook: $endpoint"

    # 3. Link SES notifications
    foreach ($type in @("Delivery", "Bounce", "Complaint")) {
        aws ses set-identity-notification-topic `
            --identity $identity `
            --notification-type $type `
            --sns-topic $topicArn
        Write-Host "✔ $type linked to $topicName"
    }

    Write-Host "✅ Setup complete for $identity\n"
}

Write-Host "🎉 All identities processed."

--

收到的 log 會放在這裡: E:\WebBackup\195\ASP.NET Project\working\Data\Log\sns

 
 
More...
Bike, 2025/4/16 上午 10:40:31
IIS ARR (reverse proxy) 服務安裝
IIS 如果要使用 reverse proxy server 服務,其實網路上已經有很多文章可以參考
這篇文章只是記錄一下安裝上要注意的事

過去安裝 IIS 套件 可以透過 Web Platform Installer 搜尋下載
但現在 IIS 的 Web Platform Installer 已經不讓人搜尋下載可安裝的套件
所以要直接去微軟網站找相關套件 可以用 IIS ARR 搜尋
https://www.iis.net/downloads/microsoft/application-request-routing
下載 requestRouter_amd64.msi 安裝這個 (3.0版 2021 年以後就沒有更新了)
安裝前,IIS也要預先安裝 URLRewrite 2 套件

安裝很簡單,msi 安裝後,IIS重啟就可以看到
IIS 的主機設定,可以看到 "Application Reuest Routing Cache"  --> 點進後右邊有 Server Proxy Settings
proxy 的設定有一些地方要注意一下,避免未來採到雷

首先 當然先開啟 Enable proxy,下面針對一些要注意的屬性說明一下

1. Time-out : 預設120秒,如果你後端的站台有一些操作可能超過兩分鐘(例如處理報表),這個就調長一點
2. Reverse rewrite host in response header: 這個勾勾預設是開的,他的好意是同站台redirect(302) 到其他網頁,可以覆蓋
    host 讓 client端能跑到正常的網址。但如果你是 redirect 到其他站台,建議把它關掉,不然後端網站如果下
    redirect (302) 到別的站台,他會主動把 redirect網址 host 改為本站 (被雷過,所以要特別記下來)
3. Include TCP port from client IP:  這是一個 X-Forwarded-For 設定,預設是打開,這樣後端主機抓 client 來源 IP就會類似
    "112.121.100.100:443" ,但後端網站在抓 client端IP通常不會管 port number,因此就會造成比對 IP 發生錯誤
    所以建議還是把它關閉
4. Enable disk cache: 預設是勾勾打開,如果後端是靜態網站,例如圖片server,這個打開沒有問題,但如果後端網站是動態網站
    那還是關掉

 
More...
darren, 2025/1/10 上午 11:01:38
Chrome 連接藍芽 BLE 印表機(出單機),使用 ESC / POS 列印繁體中文和 Qrcode
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BLE Printer Test</title>
    <script src="https://cdn.jsdelivr.net/npm/iconv-lite-umd@0.6.10/lib/iconv-lite-umd.js"></script>
</head>
<body>
    <h1>BLE Printer Test 123</h1>
    <button id="printTestButton">Test Print</button>
    <button id="printAgain">Print Again</button>
    <pre id="log"></pre>
    <script>
        // Function to log messages on the page
        function logMessage(message) {
            const logElement = document.getElementById('log');
            logElement.textContent += message + '\n';
        }
        var device;
        var server;
        var service;
        var characteristic;
        var name = 'T58_6752'; // 藍芽設備的名稱
        var uuid = 0x1800; //service UUID (用 BLE Scanner 找到的)
        var characteristicUuid = 0x2A00; // characteristic UUID (用 BLE Scanner 找到的)
        // Function to connect to BLE printer and send test data

        async function connectAndTestPrint() {

            try {
                logMessage("Requesting Bluetooth device...");

                // Request the BLE device
                device = await navigator.bluetooth.requestDevice({
                    //acceptAllDevices: true,
                    filters: [
                        { name: name }
                    ],
                    optionalServices: [uuid] // Replace with the correct service UUID
                });

                logMessage(`Device selected: ${device.name}`);
                logMessage(`Device uuid: ${device.id}`);

                await printAgain();


            } catch (error) {
                logMessage(`Error: ${error.message}`);
            }
        }

        // 转码方法
        function stringToGbk(str) {
            const ranges = [
                [0xA1, 0xA9, 0xA1, 0xFE],
                [0xB0, 0xF7, 0xA1, 0xFE],
                [0x81, 0xA0, 0x40, 0xFE],
                [0xAA, 0xFE, 0x40, 0xA0],
                [0xA8, 0xA9, 0x40, 0xA0],
                [0xAA, 0xAF, 0xA1, 0xFE],
                [0xF8, 0xFE, 0xA1, 0xFE],
                [0xA1, 0xA7, 0x40, 0xA0],
            ]
            const codes = new Uint16Array(23940)
            let i = 0

            for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
                for (let b2 = b2Begin; b2 <= b2End; b2++) {
                    if (b2 !== 0x7F) {
                        for (let b1 = b1Begin; b1 <= b1End; b1++) {
                            codes[i++] = b2 << 8 | b1
                        }
                    }
                }
            }
            const cstr = new TextDecoder('gbk').decode(codes)

            // 编码表
            const table = new Uint16Array(65536)
            for (let i = 0; i < cstr.length; i++) {
                table[cstr.charCodeAt(i)] = codes[i]
            }
            const buf = new Uint8Array(str.length * 2)
            let n = 0

            for (let i = 0; i < str.length; i++) {
                const code = str.charCodeAt(i)
                if (code < 0x80) {
                    buf[n++] = code
                } else {
                    const gbk = table[code]
                    buf[n++] = gbk & 0xFF
                    buf[n++] = gbk >> 8
                }
            }
            u8buf = buf.subarray(0, n)
            // console.log(u8buf);
            return u8buf
        }

        async function printAgain() {
            // Connect to the GATT server
            server = await device.gatt.connect();
            logMessage("Connected to GATT server.");

            // Get the printer service
            service = await server.getPrimaryService(uuid); // Replace with your printer's service UUID
            logMessage("Printer service retrieved.");

            // Get the characteristic for writing data
            characteristic = await service.getCharacteristic(characteristicUuid); // Replace with the correct characteristic UUID
            logMessage("Printer characteristic retrieved.");

            // Prepare test print data
            const encoder = new TextEncoder();
            const testData = encoder.encode("TEST PRINT: Hello from Web Bluetooth!\n");
            const finalData = encoder.encode("--\n--\n \n \n");

            const setFontSize = new Uint8Array([0x1D, 0x21, 0x11]); // GS ! n
            const setFontSize2 = new Uint8Array([0x1D, 0x21, 0x22]); // GS ! n
            const setFontSize3 = new Uint8Array([0x1D, 0x21, 0x33]); // GS ! n

            // Write test data to the printer
            logMessage("Sending test data to printer...");

            await characteristic.writeValue(new Uint8Array([0x1D, 0x21, 0x00]));
            await characteristic.writeValue(encoder.encode("1x1!\n"));
            await characteristic.writeValue(setFontSize);
            await characteristic.writeValue(encoder.encode("2x2!\n"));
            await characteristic.writeValue(setFontSize2);
            await characteristic.writeValue(encoder.encode("3x3!\n"));
            await characteristic.writeValue(setFontSize3);
            await characteristic.writeValue(encoder.encode("4x4!\n"));


            const initPrinter = new Uint8Array([0x1B, 0x40]); // ESC @
            await characteristic.writeValue(initPrinter);

            // 3. 設置字符集為 GBK
            const setGBK = new Uint8Array([0x1B, 0x74, 0x11]); // ESC t 0x11 (GBK)
            await characteristic.writeValue(setGBK);

            const text = "繁體中文測試\n \n";
            const encodedText = stringToGbk(text);
            await characteristic.writeValue(encodedText);
            logMessage("Test data sent successfully!");

            // QrCode 列印
            const qrData = "https://example.com"; // Your QR code data
            const qrDataLength = qrData.length + 3;
            const pL = qrDataLength & 0xFF; // Low byte
            const pH = (qrDataLength >> 8) & 0xFF; // High byte

            const commands = [
                0x1B, 0x40, // Initialize printer
                0x1D, 0x28, 0x6B, pL, pH, 0x31, 0x50, 0x30, ...new TextEncoder().encode(qrData), // Store data
                0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30 // Print QR code
            ];

            const buffer = new Uint8Array(commands);
            await characteristic.writeValue(buffer);
            logMessage("QrCode sent successfully!");

            await characteristic.writeValue(finalData);
            logMessage("finalData sent successfully!");

            // Disconnect the GATT server
            server.disconnect();
            logMessage("Disconnected from printer.");
        }

        // Bind the function to the button
        document.getElementById('printTestButton').addEventListener('click', connectAndTestPrint);
        document.getElementById('printAgain').addEventListener('click', printAgain);
    </script>
</body>
</html>
More...
Bike, 2025/1/2 下午 02:20:15
前端 頁面讀入 API取值工具

PromiseAll: async function (array) {
    let taskList = [];
    let propList = [];
    for (let obj of array) {
        for (let prop in obj) {
            taskList.push(obj[prop]);
            propList.push(prop);
        }
    }
    let resp = await Promise.all(taskList);
    let result = {};
    let counter = 0;
    for (let prop of propList) {
        result[prop] = resp[counter];
        counter += 1;
    }
    return result;
}

PromiseAll 是將API一次發送並接收回傳值的工具
使用方法範例:

const taskList = [{
    adPosition : BannerPositionDataService.GetList(),
    big : BannerDataService.GetList(100),
    smallTop : BannerDataService.GetOne(200),
    smallBottom : BannerDataService.GetOne(300),
    section2 : BannerDataService.GetList(400),
    section3 : BannerDataService.GetList(500),
    section4 : BannerDataService.GetList(600),
    recommends : ProductDataService.GetRndList()
}];

let resps = await UJ.PromiseAll(taskList);

要特別記得TaskList中的Method不需要做await,不然就沒有意義了


DeepBinding: function (vueData, data) {

    if (Array.isArray(data)) {
        if (!Array.isArray(vueData)) {
            vueData = [];
        } else {
            vueData.splice(0);
        }
        for (let prop in data) {
            vueData.push(data[prop]);
        }
    }
    else if (typeof (data) === 'object') {
        if (Object.keys(data).length === 0) {
            return;
        }
        for (let prop in data) {
            if (vueData[prop] === undefined || data[prop] === null ||
                (!Array.isArray(vueData[prop] && vueData !== null && typeof (data) !== 'object'))) {
                vueData[prop] = data[prop];
            } else {
                this.DeepBinding(vueData[prop], data[prop]);
            }
        }
    } else {
        vueData = data;
    }
}

在資料回傳後要Binding到Vue Data上面或是任意Object的Property上可以使用這個,
不使用DeepBinding是因為瀏覽器版本限制,這個方式不受瀏覽器版本限制,但是只有提供一階,需要多階請自行改寫.
使用方法範例:

UJ.DeepBinding(this, resp);

這邊的this代表的是Vue的Data
 
More...
梨子, 2023/11/24 上午 11:05:08
AddPath優化
改寫成可輸入多參數,效能也比較好的版本。
以下為測試碼,請自行依照專案需求做修改。

var root = "C://wdqd/qwewq";
var addPath = @"//\\/fwef/qwf";
var addPath2 = @"5fwfef/qwf";
var addPath3 = @"//fwef/qwf";
var addPath4 = @"\\\fwef/qwf";
var addPath5 = @"\\\\\/fwef/qwf";


var result = root.AddPath(addPath, addPath2, addPath3, addPath4, addPath5);

Console.WriteLine(result);

public static class Helper
{
    public static string AddPath(this string value, params string[] addPaths)
    {
        if (string.IsNullOrEmpty(value))
        {
            throw new Exception("起始目錄不可以為空字串");
        }

        if (value.Contains("..") || addPaths.Any(x => x.Contains("..")))
        {
            throw new Exception($"value: {value}, addPaths: {addPaths.Where(x => x.Contains("..")).ToOneString()} 檔名與路徑不可包含 ..");
        }

        var paths = addPaths.Select(x => x.Substring(x.FindLastContinuousCharPosition('/', '\\') + 1).SafeFilename()).ToList();

        if (paths.Any(x => System.IO.Path.IsPathRooted(x)))
        {
            throw new Exception("不可併入完整路徑 ..");
        }

        paths.Insert(0, value.SafeFilename());

        return System.IO.Path.Combine(paths.ToArray());
    }

    public static string ToOneString<T>(this IEnumerable<T> list, string separator = ",")
    {
        var strList = list.Select(x => x.ToString());
        return string.Join(separator, strList);
    }

    public static int FindLastContinuousCharPosition(this string input, params char[] targets)
    {
        int lastPosition = -1;

        for (int i = 0; i < input.Length; i++)
        {
            if (targets.Contains(input[i]))
            {
                lastPosition = i;
            }
            else
            {
                break;
            }
        }

        return lastPosition;
    }

    public static string SafeFilename(this string value)
    {
        return GetValidFilename(value);
    }

    public static string GetValidFilename(string value)
    {
        string ValidFilenameCharacters = @"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\-_$.@:/# ";
        if (value.Contains(".."))
        {
            throw new Exception("路徑中不可包含 .. ");
        }

        string newUrl = "";
        for (int i = 0; i < value.Length; i++)
        {
            var c = value.Substring(i, 1);
            int k = ValidFilenameCharacters.IndexOf(c);
            if (k < 0)
            {
                throw new Exception($"檔名 '{value}' 中有非法的字元 '" + c + "'。");
            }
            newUrl += ValidFilenameCharacters.Substring(k, 1);
        }

        return newUrl;
    }
}
More...
梨子, 2023/8/28 上午 09:43:49
sessionStorage 跨分頁無法讀取
問題:
在開出新分頁後,無法讀取 sessionStorage 的資料


可能原因:
Chrome 某個版本,Stop cloning sessionStorage for windows opened with noopener
a标签_blank默认 rel="noopener" ,所以a标签需要加入rel=“opener” 而才能像window.open("同源页面")这种方式新开的页面会复制之前的sessionStorage


解決方法:
開新分頁前,加入 rel=“opener”參數即可。
例:
<a href="http://..." target="_blank" rel="opener">Link</a>

$(".hlkPrint").click(function () {
    $("form").setPostDataToStorage();

    $("form").attr("rel", "opener");
    $("form").attr("target", "_blank");
    $("form").attr("action", "xxxxx.aspx");
    $("form").submit();
});





參考:
面试官:你确定多窗口之间sessionStorage不能共享状态吗???
More...
Reiko, 2022/12/1 下午 05:57:23
關於 PostgreSQL
-- 自動更新某個欄位

CREATE OR REPLACE FUNCTION before_market_update ()
RETURNS TRIGGER AS 
$$
BEGIN
    NEW.LMDT = LocalTimeStamp;
    RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

CREATE TRIGGER updt_market
  BEFORE UPDATE
  ON "Market"
  FOR EACH ROW
  EXECUTE PROCEDURE before_market_update();
More...
Bike, 2022/8/12 下午 09:25:57
PostgreSQL 的 monitor trigger
--建立 table (要記得改 XXX)
CREATE TABLE IF NOT EXISTS public.table_monitor
(
    table_name text COLLATE pg_catalog."default" NOT NULL,
    update_at timestamp without time zone NOT NULL DEFAULT now(),
    update_count bigint NOT NULL DEFAULT 0,
    CONSTRAINT table_monitor_pkey PRIMARY KEY (table_name)
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public.table_monitor
    OWNER to XXX;

-- 這一段只要執行一次
CREATE OR REPLACE FUNCTION public.table_monitor_trigger_fnc()
    RETURNS trigger
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE NOT LEAKPROOF
AS $BODY$
BEGIN
     Update public.table_monitor Set update_count = update_count + 1, update_at = now()  Where table_name = TG_TABLE_NAME; 
RETURN NEW;
END;
$BODY$;

ALTER FUNCTION public.table_monitor_trigger_fnc()
    OWNER TO XXX;

-- 對於被監控的 table, 要執行以下的指令,記得 monitor_table_name 要改為 table 的名字(要符合大小寫)
INSERT INTO public.table_monitor (table_name, update_at, update_count) VALUES ('monitor_table_name', '2000-01-01', 0) ON CONFLICT DO NOTHING;

CREATE OR REPLACE TRIGGER table_monitor_trigger
    AFTER INSERT OR DELETE OR UPDATE 
    ON monitor_table_name
    FOR EACH STATEMENT
    EXECUTE FUNCTION public.table_monitor_trigger_fnc();

監控用的程式碼:
public static void UpdateTableCache()
{
    //標記自已是正在執行的 Thread. 讓舊的 Thread 在執行完畢之後, 應該會自動結束. 以防舊的 Thread 執行超過 10 秒.
    CurrentThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

    try
    {
        //確認自已是正在執行的 Thread, 重覆執行. (另一個 Thread 插入執行)
        while (CurrentThreadId == System.Threading.Thread.CurrentThread.ManagedThreadId)
        {
            LastUpdateDate = DateTime.Now;

            foreach (var dbId in CachedDbs)
            {
                var sql = @"select * from table_monitor";

                var dt = Su.PgSql.DtFromSql(sql, dbId);

                foreach (DataRow row in dt.Rows)
                {
                    string changeId = row["update_count"].DBNullToDefault();
                    string CacheKey = TableCacheKey(dbId, row["table_name"].DBNullToDefault());
                    ObjectCache cache = MemoryCache.Default;

                    string OldValue = (string)cache[CacheKey];

                    if (OldValue == null)
                    {
                        cache.Set(CacheKey, changeId, DateTime.MaxValue);
                    }
                    else
                    {
                        if (changeId != OldValue)
                        {
                            cache.Remove(CacheKey);
                            cache.Set(CacheKey, changeId, DateTime.MaxValue);
                        }
                    }
                }
            }

            //每兩秒檢查一次
            System.Threading.Thread.Sleep(2000);
        }
    }
    catch (Exception)
    {
        //依經驗, 只要 DB 能通, 這裡幾乎不會有問題, 所以這裡暫時不處理, 未來有問題時可以考慮寫入文字檔比較好.
    }
}
More...
Bike, 2022/7/20 上午 10:05:39
.Net Framework 的檔案檢查機制 Web API 和 MVC 5
我們的目標是在啟動時,掛一個 global filter 來檢查上傳的檔案附檔名,避免上傳未預期的可執行檔。使用 global filter 可以防止程式設計師忘了做檔案檢查,發生危險。我們目前用到的 .Net Framework 有兩個類別,一個是 System.Web.Http.ApiController,另一個是 System.Web.Mvc.Controller,兩個要分開處理。

1. Web API (System.Web.Http.ApiController)
.Net Framework 的 Web API 2 在接收檔案時,必需使用 Request.Content.ReadAsMultipartAsync 把上傳的資料存入 MultipartMemoryStreamProvider 之後再來處理。
範例如下: 
    public class UploadController : ApiController
    {
        public async Task<object> PostFormData()
        {
            var provider = new MultipartMemoryStreamProvider();

            if (! Request.Content.IsMimeMultipartContent())
            {
                return "no file";
            }

            //要注意這裡的 await
            await Request.Content.ReadAsMultipartAsync(provider);

            foreach (var content in provider.Contents)
            {
                if (content.Headers.ContentDisposition.FileName != null)
                {
                    string localFilename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
                    System.IO.Directory.CreateDirectory(HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/"));
                    string filename = HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/" + localFilename);
                    if (System.IO.File.Exists(filename))
                    {
                        System.IO.File.Delete(filename);
                    }

                    using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write))
                    {
                        var contentStream = await content.ReadAsStreamAsync();
                        await contentStream.CopyToAsync(fileStream);
                        Trace.WriteLine("Save To" + filename);
                    }
                }
            }
            return "OK";
        }
    }


程式碼中有個地方我沒有注意到。我一開始是用 Request.Content.ReadAsMultipartAsync(provider); 把資料讀進 provider,但前方沒有加上 await,造成有時讀不到檔案的情況,再次提醒自己 Async 和 Await 的關係。

直覺的想法是在 filter 中執行一樣的程式碼,不存檔,只要抓出檔名來檢查即可。但這時遇到一個問題 Request.Content.ReadAsMultipartAsync 不能執行兩次,Google 了一陣子,也沒找不到類似 seek(0) 的操作。所以只好自己寫一個暫存的機製。

這裡決定使用 System.Web.HttpContext.Current.Items 來暫存 provider,最後的 OnActionExecutingAsync 結果如下:

        /// <summary>
        /// 檔案檢查
        /// </summary>
        /// <param name="actionContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            Trace.WriteLine("ApiCheckFile OnActionExecutingAsync");

            var request = actionContext.Request;
            if (!request.Content.IsMimeMultipartContent())
            {
                return;
            }

            var provider = new MultipartMemoryStreamProvider();
            await request.Content.ReadAsMultipartAsync(provider);

            //把 provider 存入 System.Web.HttpContext.Current.Items 之中,以便在 controller 中再度使用
            System.Web.HttpContext.Current.Items["MimeMultipartContentProvider"] = provider;

            foreach (var content in provider.Contents)
            {
                if (content.Headers.ContentDisposition.FileName != null)
                {
                    var filename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
                    Trace.WriteLine(filename);

                    var ext = System.IO.Path.GetExtension(filename);
                    if (!".jpg,.jpeg,.png".Contains(ext.ToLower()))
                    {
                        throw new Exception("file format error.");
                    }
                }
            }

            return;
        }


而 controller 中的 Action 則改為以下的版本;本版本中,除了把上傳的檔案存檔以外,還有讀取 form data 中其它欄位的範例:

        public async Task<object> PostFormData()
        {
            //改由 HttpContext.Current.Items 中,讀取資料。
            MultipartMemoryStreamProvider provider = (MultipartMemoryStreamProvider)System.Web.HttpContext.Current.Items["MimeMultipartContentProvider"];

            //如果沒有經過 filter,provider會是 null, 這時就要直接由 Request.Content 讀入 provider
            if (provider == null)
            {
                provider = new MultipartMemoryStreamProvider();
                Request.Content.ReadAsMultipartAsync(provider);
            }

            foreach (var content in provider.Contents)
            {
                if (content.Headers.ContentDisposition.FileName != null)
                {
                    string localFilename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
                    Trace.WriteLine("FileName: " + localFilename);
                    Trace.WriteLine("FileName: " + @"~/App_Data/Temp/" + localFilename);
                    System.IO.Directory.CreateDirectory(HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/"));
                    string filename = HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/" + localFilename);
                    if (System.IO.File.Exists(filename))
                    {
                        System.IO.File.Delete(filename);
                    }

                    using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write))
                    {
                        var contentStream = await content.ReadAsStreamAsync();
                        await contentStream.CopyToAsync(fileStream);
                        Trace.WriteLine("Save To" + filename);
                    }
                }
                else
                {
                    var contentStream = await content.ReadAsStreamAsync();
                    var reader = new System.IO.StreamReader(contentStream);
                    var data = reader.ReadToEnd();
                    Trace.WriteLine("data: " + data);
                }
            }
            return "OK";
        }


以上是展示 Web Api 的 Filter。

如果是 MVC 的 Controller,就簡單多了,直接讀取 actionContext.HttpContext.Request.Files 裡面各檔案的 FileName 即可。不過也遇到了一個和直覺不同的地方,對 actionContext.HttpContext.Request.Files 居然不能用 foreach 請見下方註解掉的地方。也許是我的用法有問題,如果有人找到可以使用的方法也麻煩告知。

        public override void OnActionExecuting(ActionExecutingContext actionContext)
        {
            Debug.WriteLine("MvcCheckFileFilter OnActionExecuting");
            if (actionContext.HttpContext.Request.Files.Count > 0)
            {
                for (int i = 0; i < actionContext.HttpContext.Request.Files.Count; i++)
                {
                    System.Web.HttpPostedFileBase file = actionContext.HttpContext.Request.Files[i];
                    if (System.IO.Path.GetExtension(file.FileName) != ".jpg")
                    {
                        throw new Exception("file format error.");
                    }
                    Debug.WriteLine(i + "MvcCheckFileFilter OnActionExecuting File type: " + file.FileName.ToString());
                }
            }

            //以下寫法會發生錯誤: 無法將類型 'System.String' 的物件轉換為類型 'System.Web.HttpPostedFileBase'。
            //foreach (HttpPostedFileBase file in actionContext.HttpContext.Request.Files)
            //{
            //    if (System.IO.Path.GetExtension(file.FileName) != ".jpg")
            //    {
            //        throw new Exception("file format error.");
            //    }
            //}
        }


最後是把 filter 掛在 global filter 裡,如下:
Web API Controller 的部份:
在 WebApiConfig.cs 裡加上 config.Filters.Add(new ApiCheckFile()); 見下圖:
 

附帶一提,Register 是在 global.asax 裡被叫用的。微軟的架構常常這樣,硬是拉到另一個目錄的獨立檔案裡,如果 global.asax 沒有這一行,不知所以的人,在 App_Start 裡,建立了一個 WebApiConfig.cs 然後會發現完全沒做用... 見下圖

 
MVC Controller 的部份,
在 FilterConfig.cs 中,加上 filters.Add(new MvcCheckFileFilter()); 如下圖
 

一樣是在 global.asax 中呼叫  FilterConfig.RegisterGlobalFilters,見下圖:

 
不過有趣的地方是,在 global.asax 裡, WebApiConfig.Register 和 FilterConfig.RegisterGlobalFilters 的叫用方法不同,一個是把 function 當作變數,一個是直接呼叫 function, 大家可以比較一下。

附註 1, 關於 OnActionExecutingAsync 和 OnActionExecuting:

Web API 的 filter 中,提供了 OnActionExecutingAsync 和 OnActionExecuting 兩個 method 可以 override ,我實驗的結果是如果兩個 method 都 override 了,只有 OnActionExecutingAsync 會被呼叫。

MVC 的 filter 中,只有 OnActionExecuting 可以 override。
 
More...
Bike, 2022/5/15 上午 10:01:11
|< 12345 >|
頁數 1 / 5 下一頁
~ Uwinfo ~