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
標籤
  • 網路名稱
  • 5275
  • date[t]
  • 威勝
  • 檔案命名
  • 6903
  • timeout
  • 4027
  • groupby
  • IDENTITY
  • 現網站在大流量卡住時
  • 憑証
  • m0wjbho4
  • git
  • vb.net
  • 82
  • 0KeeTeam|e
  • npoi
  • for
  • 移至定義
  • aspx[t]
  • ef
  • ubuntu
  • sa
  • 寬度
  • SQL2008
  • url2121121
  • message
  • win
  • asp
  • timeout212
  • 826
  • HTTP 錯èª
  • 58
  • iframe
  • CCE-44186-
  • 網址
  • 空格
  • fortinet
  • vpn
  • 排程
  • MSkvKiovQU
  • CS
  • 搜尋
  • ddd
  • 站內
  • minvalue[t
  • dbcontext
  • C
  • -5809
頁數 1 / 2 下一頁
搜尋 google 結果:
ubuntu server安裝配置
ubuntu安裝時依照版本不同有些會需要先配置mirror site
mirror site的功用是提供下載套件與系統更新的站台
而要連網確認mirror site會需要配置網路接口,DNS與開放防火牆
17.10以後版本可前往 /etc/netplan 底下的.yaml文件內查看設置
相關參數說明↓
addresses: 對外ip (ex: 192.168.1.10/24)
gateway4: 網路關口, IPv4從本機連結網際網路的端口, 通常設定路由器的IP地址
nameservers: 底下的 addresses為DNS位置, 可設置 8.8.8.8 (google的公共服務器)
設置完後執行 sudo netplan apply變更配置
17.10以前的自己去問GPT, 配置差不多只是位置不一樣

輸入'systemd-resolve --status'查看配置
輸入'ping google.com'測試網路是否正常

以上配置完畢 防火牆有開ubuntu以後可以配置mirror site↓
ubuntu官方承認的Site清單
*注意!! 更新封包皆會從設定的Site取得, 雖是官方認證但是仍有風險, 有高度資安需求建議自行架設mirror site*
mirror site的配置於 /etc/apt/sources.list
'deb http://{domain}' 的皆需修改
可使用 ctrl + \ 來一次修改
完成後執行 'sudp apt-get update' 測試mirror site是否連通
More...
梨子, 2023/12/6 上午 10:39:22
合併兩個 Expression -- Combining two expressions (Expression>)
找了很久,原來就在 梨子給的範例裡。

假設有兩個 expression: e1, e2

            var combineBody = Expression.AndAlso(e1.Body, Expression.Invoke(e2, e1.Parameters[0]));
            var finalExpression = Expression.Lambda<Func<TestClass, bool>>(combineBody, e1.Parameters).Compile();


同理,把上面的 AndAlso 換成 OrElse 就可以用 Or 合併。

即使只有兩行,還是不太可能背起來,所以當然要來做一下擴充

    public static class ExpressionExtension
    {
        public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> e1, Expression<Func<T, bool>> e2)
        {
            var combineE = Expression.AndAlso(e1.Body, Expression.Invoke(e2, e1.Parameters[0]));

            return Expression.Lambda<Func<T, bool>>(combineE, e1.Parameters);
        }

        public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> e1, Expression<Func<T, bool>> e2)
        {
            var combineE = Expression.OrElse(e1.Body, Expression.Invoke(e2, e1.Parameters[0]));

            return Expression.Lambda<Func<T, bool>>(combineE, e1.Parameters);
        }
    }


使用範例:

            Expression<Func<Market, bool>> e1 = e => e.Is_Deleted == isDelete;

            Expression<Func<Market, bool>> e2 = e => string.IsNullOrEmpty(marketNo) || e.MarketNo.ToUpper().Contains(marketNo.ToUpper());

            return new
            {
                andMarkets = Ds.PageContext.ShopBandContext.Markets.Where(e1.AndAlso(e2)).ToList(),
                orMarkets = Ds.PageContext.ShopBandContext.Markets.Where(e1.OrElse(e2)).ToList(),
            };



 再補兩個擴充, 可以把多個 Expression 用 AndAlso 或 OrElse 串在一起:

        public static Expression<Func<T, bool>> OrElseAll<T>(this IEnumerable<Expression<Func<T, bool>>> exps)
        {
            if (exps.Count() == 1)
            {
                return exps.First();
            }

            var e0 = exps.First();

            var orExp = exps.Skip(1).Aggregate(e0.Body, (x, y) => Expression.OrElse(x, Expression.Invoke(y, e0.Parameters[0])));

            return Expression.Lambda<Func<T, bool>>(orExp, e0.Parameters);
        }

        public static Expression<Func<T, bool>> AndAlsoAll<T>(this IEnumerable<Expression<Func<T, bool>>> exps)
        {
            if (exps.Count() == 1)
            {
                return exps.First();
            }

            var e0 = exps.First();

            var orExp = exps.Skip(1).Aggregate(e0.Body, (x, y) => Expression.AndAlso(x, Expression.Invoke(y, e0.Parameters[0])));

            return Expression.Lambda<Func<T, bool>>(orExp, e0.Parameters);
        }


使用範例:

            Expression<Func<Market, bool>> q = e =>
            e.Is_Deleted == "N"
            && (string.IsNullOrEmpty(marketNo) || e.MarketNo.ToLower().Contains(marketNo.ToLower()))
            && (string.IsNullOrEmpty(isCombination) || isCombination != "Y" || e.TypeEnum == 200);

            if (!string.IsNullOrEmpty(name))
            {
                var nameList = name.Split(',').Select(e => e.Trim())
                    .Where(e => !string.IsNullOrEmpty(e));

                if (nameList.Any())
                {
                    q = q.AndAlso(nameList
                        .Select(s => (Expression<Func<Market, bool>>)(e => e.Name.ToLower().Contains(s.ToLower())))
                        .OrElseAll());
                }
            }






另外在 google,整理了 stackoverflow 幾篇文章之後得到的另一個方法,比較複雜, 不過可以讓人理解一下 Expression 比較底層的東西,也留下來參考一下。

    internal class MergeTool : ExpressionVisitor
    {
        private readonly ParameterExpression _parameter;

        protected override Expression VisitParameter(ParameterExpression node)
        {
            return base.VisitParameter(_parameter);
        }

        internal MergeTool(ParameterExpression parameter)
        {
            _parameter = parameter;
        }

        public static Expression<Func<T, bool>> MergedExpression<T>(Expression<Func<T, bool>> e1, Expression<Func<T, bool>> e2)
        {
            ParameterExpression param = Expression.Parameter(typeof(T));

            BinaryExpression MergeBody = Expression.AndAlso(e1.Body, e2.Body);

            var ReplacedBody = (BinaryExpression)new MergeTool(param).Visit(MergeBody);

            return Expression.Lambda<Func<T, bool>>(ReplacedBody, param);
        }
    }

使用時要 Compile

            var mergedExpression = MergeTool.MergedExpression(e1, e2);

            var list = testList.Where(mergedExpression.Compile());

More...
Bike, 2022/8/13 下午 05:53:28
.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
使用 Gmail API 及 Google API 憑證的取得流程
這裡是我測試 Gmail API 和 Google API 憑証的一些記錄。

如果你的目的是要使用 Gmail Api 取代舊的 Gmail SMTP 來發送通知信,建議你先跳到最下方看一下結論。

如果你是想要看一下 Gmail API 和 Google API 憑証的使用方法,可以看一下這篇文章。

1. 在 google cloud platform 建立新的專案.
https://console.cloud.google.com/

 
 



啟用 Gmail API







 



 


 
 



因為我們要透過 OAuth  取得使用者授權,所以要設定使用 OAuth 的同意畫面。

 
指定授權的範圍
 

 
因為剛建立的專案,不會被公開,所以要指定測試使用者
 
如果要給任意使用者,必需經過發布的流程,但準備工作有點麻煩,所以這次就不發布了。
 




建立 OAuth 2.0 用戶端 ID 憑証
 
這裡除了名稱外,還有一個設定重導 Uri 的項目。現在不填寫,但稍後要回來補這個資料。
 
 
 下載 json 之後,命名為 client_secret.json 保留後續使用。


再來就要建立專案了. 用 VS2022 建立一個新專案
  


 




記錄網址, 本測試專案是 https://localhost:44340/ ,請依實際網址為準。



回到 OAuth 2.0 用戶端 ID 的設定頁. 在已授權的重新導向 URI 中填入 https://localhost:44340/Home/AuthReturn (填入的網址依實際專案的狀況,可能會有變化)
 
 

在 VS2022 中,使用 Nuget 安裝套件: (有漏的再麻煩和我說)
Google.Apis.Gmail.v1
Google.Apis.Auth
MimeKit (發送 gmail 時使用)

建立認証用的網址:
建立一個 Action, 用來取得認証用的網址:
        /// <summary>
        /// 取得授權的項目
        /// </summary>
        static string[] Scopes = { GmailService.Scope.GmailSend };

        // 和登入 google 的帳號無關
        // 任意值,若未來有使用者認証,可使用使用者編號或登入帳號。
        string Username = "ABC";

        /// <summary>
        /// 存放 client_secret 和 credential 的地方
        /// </summary>
        string SecretPath = @"D:\project\GmailTest\Data\Secrets";

        /// <summary>
        /// 認証完成後回傳的網址, 必需和 OAuth 2.0 Client Id 中填寫的 "已授權的重新導向 URI" 相同。
        /// </summary>
        string RedirectUri = $"https://localhost:44340/Home/AuthReturn";

        /// <summary>
        /// 取得認証用的網址
        /// </summary>
        /// <returns></returns>
        public async Task<string> GetAuthUrl()
        {
            using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
            {
                FileDataStore dataStore = null;
                var credentialRoot = Path.Combine(SecretPath, "Credentials");
                if (!Directory.Exists(credentialRoot))
                {
                    Directory.CreateDirectory(credentialRoot);
                }

                //存放 credential 的地方,每個 username 會建立一個目錄。
                string filePath = Path.Combine(credentialRoot, Username);
                dataStore = new FileDataStore(filePath);

                IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
                    Scopes = Scopes,
                    DataStore = dataStore
                });

                var authResult = await new AuthorizationCodeWebApp(flow, RedirectUri, Username)
                .AuthorizeAsync(Username, CancellationToken.None);

                return authResult.RedirectUri;
            }
        }


執行結果:
 
用 chrome 開啟產生的網址: 


選取任一帳號,如果出現以下錯誤,請回到 "OAuth 同意畫面" 去新增測試使用者


 
因為應用程式尚未發布,所以會看到警告,勇敢的繼續下去


這裡會要求授權使用你的名義發送信件。(這是在程式中取得授權的項目 Scopes 中所指定的)


再繼續之後,會被重導至我們在 redirectUri  指定的網址。因為我們尚未完成,所以會看到錯誤,順便也可以看一下,會帶回哪一些參數。有 state, code, scope,共三個。


順便看一下,google 的套件會在 Credentials 的目錄下幫使用者建立一個目錄,在完成驗証前,會先放一個 System.String-oauth_XXX 的檔案,裡面的值和回傳的 state 是一樣的,這個應該是用來驗証回傳資料的。



接下來我們要新增 Action "AuthReturn" 如下:

        public async Task<string> AuthReturn(AuthorizationCodeResponseUrl authorizationCode)
        {
            string[] scopes = new[] { GmailService.Scope.GmailSend };

            using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
            {
                //確認 credential 的目錄已建立.
                var credentialRoot = Path.Combine(SecretPath, "Credentials");
                if (!Directory.Exists(credentialRoot))
                {
                    Directory.CreateDirectory(credentialRoot);
                }

                //暫存憑証用目錄
                string tempPath = Path.Combine(credentialRoot, authorizationCode.State);            

                IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(
                new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
                    Scopes = scopes,
                    DataStore = new FileDataStore(tempPath)
                });

                //這個動作應該是要把 code 換成 token
                await flow.ExchangeCodeForTokenAsync(Username, authorizationCode.Code, RedirectUri, CancellationToken.None).ConfigureAwait(false);

                if (!string.IsNullOrWhiteSpace(authorizationCode.State))
                {                
                    string newPath = Path.Combine(credentialRoot, Username);
                    if (tempPath.ToLower() != newPath.ToLower())
                    {
                        if (Directory.Exists(newPath))
                            Directory.Delete(newPath, true);

                        Directory.Move(tempPath, newPath);
                    }
                }

                return "OK";
            }
        }


再跑一次上面的流程,最後回到 AuthReturn

在 D:\project\GmailTest\Data\Secrets\Credentials\ABC 裡面會產生一個檔案: 這個就是我們的 token 了。


看一下裡面的內容, 有 access_token, refresh_token, scope 等等, 用途應該很好猜了.. 不知道各項目的目途也沒有關係。只要有這個 token 就可以了。


refresh_token 的效期請參考以下文件:
https://developers.google.com/identity/protocols/oauth2 。也可以參考下圖, 若是要用 gmail api 來發送通知信(例如連絡我們),紅色的地方是比較令人困擾的,例如 6 個月以上,沒有人留言,原來留下的 refresh_token 就失效了。使用者必需重新建立一個 refresh_token 。

 
最後來使用 gmail api 發送通知信, 直接看程式碼如下: 在這個過程中遇到最大的問題除了憑証問題之外,另一個問題是編碼。直到最後找到可以用 MimeKit 把 System.Net.Mail.MailMessage 編碼成 Gmail API 的格式才解決。程式碼如下:

        public async Task<bool> SendTestMail()
        {
            var service = await GetGmailService();

            GmailMessage message = new GmailMessage();
            message.Subject = "標題";
            message.Body = $"<h1>內容</h1>";
            message.FromAddress = "bikehsu@gmail.com";
            message.IsHtml = true;
            message.ToRecipients = "bikehsu@gmail.com";
            message.Attachments = new List<Attachment>();

            string filePath = @"C:\Users\bike\Pictures\Vegetable_pumpkin.jpg";    //要附加的檔案
            Attachment attachment1 = new Attachment(filePath);
            message.Attachments.Add(attachment1);

            SendEmail(message, service);
            Console.WriteLine("OK");

            return true;
        }

        async Task<GmailService> GetGmailService()
        {
            UserCredential credential = null;

            var credentialRoot = Path.Combine(SecretPath, "Credentials");
            if (!Directory.Exists(credentialRoot))
            {
                Directory.CreateDirectory(credentialRoot);
            }

            string filePath = Path.Combine(credentialRoot, Username);

            using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
            {
                credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                GoogleClientSecrets.Load(stream).Secrets,
                Scopes,
                Username,
                CancellationToken.None,
                new FileDataStore(filePath));
            }

            var service = new GmailService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Send Mail",
            });

            return service;
        }


        public class GmailMessage
        {
            public string FromAddress { get; set; }
            public string ToRecipients { get; set; }

            public string Subject { get; set; }
            public string Body { get; set; }
            public bool IsHtml { get; set; }

            public List<System.Net.Mail.Attachment> Attachments { get; set; }
        }


        public static void SendEmail(GmailMessage email, GmailService service)
        {
            var mailMessage = new System.Net.Mail.MailMessage();
            mailMessage.From = new System.Net.Mail.MailAddress(email.FromAddress);
            mailMessage.To.Add(email.ToRecipients);
            mailMessage.ReplyToList.Add(email.FromAddress);
            mailMessage.Subject = email.Subject;
            mailMessage.Body = email.Body;
            mailMessage.IsBodyHtml = email.IsHtml;

            if (email.Attachments != null)
            {
                foreach (System.Net.Mail.Attachment attachment in email.Attachments)
                {
                    mailMessage.Attachments.Add(attachment);
                }
            }

            var mimeMessage = MimeKit.MimeMessage.CreateFromMailMessage(mailMessage);

            var gmailMessage = new Google.Apis.Gmail.v1.Data.Message
            {
                Raw = Encode(mimeMessage)
            };

            Google.Apis.Gmail.v1.UsersResource.MessagesResource.SendRequest request = service.Users.Messages.Send(gmailMessage, "me");

            request.Execute();
        }

        public static string Encode(MimeMessage mimeMessage)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                mimeMessage.WriteTo(ms);
                return Convert.ToBase64String(ms.GetBuffer())
                    .TrimEnd('=')
                    .Replace('+', '-')
                    .Replace('/', '_');
            }
        }



收到的信件:



結論:
使用 Gmail API 最大的原因是要增加安全性,和舊的 smtp 不同的地方是,使用 gmail api 之後,客戶不需要提供 gmail 的帳號和密碼就可以讓系統使用 gmail 發送信件,不過由於 refresh_token 的效期問題,可能會造成無法發送通知信而沒有任何人發現的情況,整個實用性會變的很低。

另一個還沒有測試的部份是應用程的啟用。這個審核不知道會不很麻煩,不過可以而知的時,整個流程會花更多的時間。

取代的做法: 可能要改用 Amazon 的 SES 來寄信,而且為了避免每個小網站都要跑 SES 的建立流程,準備來寫一個 API 給各網站使用,可以發送簡單的通知信。

以上的程式碼可以在這裡下載: https://github.com/bikehsu/GmailTest
More...
Bike, 2022/4/10 下午 09:31:15
Oracle 建立可以自動成長的 table space
感覺上, Oracle 的 table space 和 MS-SQL 的 DB 是一樣的。都是用來指定實體檔案。

google 到的範例居然有小 bug, 記錄一個修正版如下: 

CREATE TABLESPACE TABPE_SPACE_NAME
   DATAFILE 'TABPE_SPACE_NAME.dbf'
   SIZE 1m
   AUTOEXTEND ON;

完成之後,可以列出所有 table space:

SELECT A.TABLESPACE_NAME, A.FILE_NAME,
  ROUND(B.FREE_GB,2) AS FREE_GB , 
  ROUND(A.TOTAL_GB - b.FREE_GB) AS USED_GB ,
  ROUND(A.TOTAL_GB,2) AS TOTAL_GB , 
  ROUND(((A.TOTAL_GB - B.FREE_GB)/ A.TOTAL_GB )*100,2) AS USED_PERCENT,
  ROUND((B.FREE_GB/ A.TOTAL_GB )*100,2) AS FREE_PERCENT
FROM   
  (
    SELECT TABLESPACE_NAME, FILE_NAME,
      SUM(BYTES)/ (1024*1024*1024) AS TOTAL_GB
    FROM DBA_DATA_FILES  
    -- WHERE TABLESPACE_NAME='My_TableSpace_Name'
    GROUP  BY TABLESPACE_NAME, FILE_NAME
  )A,
  (
    SELECT TABLESPACE_NAME,    
      SUM(BYTES) / (1024*1024*1024) AS FREE_GB 
    FROM DBA_FREE_SPACE 
    -- WHERE  TABLESPACE_NAME='My_TableSpace_Name'
    GROUP BY TABLESPACE_NAME
   ) B
WHERE A.TABLESPACE_NAME= B.TABLESPACE_NAME ;
More...
Bike, 2022/4/6 上午 08:35:54
使用Lucene.Net達成全文檢索!基礎解說(一)
Lucene.Net是一套C#開源全文索引庫,其主要包含了:
· Index : 提供索引的管理與詞組的排序
· Search : 提供查詢相關功能
· Store : 支援資料儲存管理,包括I/O操作
· Util : 共用套件
· Documents : 負責描述索引儲存時的文件結構管理
· QueryParsers : 提供查詢語法
· Analysis : 負責分析內容
要達到高效能的全文檢索讓機器可以明白我們的語言,最重要的關鍵就是"分詞器"了。
試想一下這一句話你會如何拆分成一段一段的關鍵字呢?
"一天一蘋果,醫生遠離我"
還有英文版本
"An apple a day, doctor keep me away."
中文版本的拆分:
"一天"、"一"、"蘋果"、"醫生"、"遠離"、"我"
英文版本的拆分:
"apple"、"day"、"doctor"、"keep"、"me"、"away"
有沒有注意到不同語系所分析出來的關鍵字有一點不一樣呢?
而在Lucene中分詞的工作會交給Analysis來完成,
不過我們可以依照不同的語系去選擇想使用的分詞器(Analyzer)!

首先簡單說明一下Lucene的實作流程
1. 確認主要搜尋的語系來決定使用的分詞器(analyzer)
2. 建立Document依照analyzer匯入資料
(前置完成)
3. 建立IndexSearcher導入準備好的Document
4. 建立Parser來分析SearchValue
5. 使用IndexSearcher分析Parser取得結果(Hits)
*本專案使用的是Lucene.Net 3.0.3*
接下來我們來建立一個提供查詢使用的Document。

       
 // 取得或建立Lucene文件資料夾
        if (!File.Exists(_dir.FullName))
        {
            System.IO.Directory.CreateDirectory(_dir.FullName);
        }
        // Asp.Net Core需要於Nuget安裝System.Configuration.ConfigurationManager提供用戶端應用程式的組態檔存取
        Lucene.Net.Store.Directory directory = FSDirectory.Open(_dir);
        // 選擇分詞器
        var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_CURRENT);
        // 資料來源
        var repository = new Repository();
        // 依照指定的文件結構來建立
        var indexWriter = new IndexWriter(directory, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);
        foreach (var index in repository)
        {
            var document = new Document();
            document.Add(new Field("Id", index.Id.ToString(), Field.Store.YES, Field.Index.NO));
            document.Add(new Field("Name", index.Name, Field.Store.YES, Field.Index.ANALYZED));
            document.Add(new Field("Description", index.Description, Field.Store.NO, Field.Index.ANALYZED));
            indexWriter.AddDocument(document);
        }
        indexWriter.Optimize();
        indexWriter.Commit();
        indexWriter.Dispose();


如此一來我們就建立好Lucene的基本配備囉!
其中analyzer的部分我們使用Lucene.Net預設,
要特別注意的是,其處理中文語系的能力非常之爛!
之後再寫一篇文章深入探討。
再來值得一提的是
       
document.Add(new Field("Id", index.Id.ToString(), Field.Store.YES, Field.Index.NO));

前兩個參數就是Key跟Value,可以簡單理解為欄位與其內容。
後面兩個參數是重點!
Store: 代表是否儲存這個Key的Value
例如在google打上台南美食會搜索出許多不同的文章連結,
不過google給你的資料中最重要的不是文章內容(Description),
而是哪一篇文章(Name)與台南美食最有關係。
假如今天我只要回傳一個列表而不用提示文章中有哪些內容,
那麼我就可以選擇給"Description" Field.Store.No來節省空間。
Index:
· NO - 不加入索引,這個內容只需要隨著結果出爐,不需要在查詢的時候被考慮。
· ANALYZED、NOT_ANALYZED - 是否使用分詞
· NO_NORMS - 關閉權重功能
或許許多人會對權重功能(NORMS)感到疑惑,
簡單的舉個例子
{ Id=1, Key="蘋果", Value="一天一蘋果,醫生遠離我。"}
{ Id=2, Key="橘子", Value="醫生給娜美最珍貴的寶藏。"}
{ Id=3, Key="梨子", Value="我是梨子,比蘋果蘋果好吃多囉!"}
當我搜尋"蘋果"的時候結果會是
{ Id=1, MatchKey=1, MatchValue=1, Score=(1*5) + (1*2) = 7}
{ Id=3, MatchKey=0, MatchValue=1, Score=(0*5) + (2*2) = 4}
有發現了嗎?
雖然同樣都對中兩個結果但是Id 1的資料Key值中有包含關鍵字,
因此得到較高的分數排在Id 3前方
準備好Document了,我們可以開始來實際使用看看囉!

       
// 決定所要搜索的欄位
        var parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_CURRENT, "Description", analyzer).Parse(searchValue);
        // 提供剛剛建立的Document
        var indexSearcher = new IndexSearcher(directory);
        // 搜尋取出結果的數量
        var queryLimit = 20;
        // 開始搜尋!
        var hits = indexSearcher.Search(parser, queryLimit);
        if (!hits.ScoreDocs.Any())
        {
            Console.WriteLine("查無相關結果。");
            return;
        }
        Document doc;
        foreach (var hit in hits.ScoreDocs)
        {
            doc = indexSearcher.Doc(hit.Doc);
            Console.WriteLine("Score :" + hit.Score + ", Id :" + doc.Get("Id") + ", Name :" + doc.Get("Name") + ", Description :" + doc.Get("Description"));
        }


最後的結果(Hits),是需要再回到Document去撈出對應的資料喔!
是不是非常簡單呢?
筆者寫了一個簡單的範例在GitHub上,秉持著追求新技術的心使用了.Net 6,還請各位大大多多包涵。
有中英文兩種Repository,只需要在上方的DI注入切換就可以囉!
GitHub連結: https://github.com/g13579112000/Lucene
筆者第一次撰寫這種教學文章,有哪邊錯誤的非常歡迎一起來討論指教。
之後有機會再撰寫Lucene更深入的應用方面,
例如權重的分配與分詞器的選擇與使用。
感謝您的閱讀。


參考文獻:
1.黑暗大大的全文檢索筆記: https://blog.darkthread.net/blog/lucene-net-notes-1/
2.使用.Net實現全文檢索: https://blog.csdn.net/huwei2003/article/details/53408388
3.伊凡的部落格: http://irfen.me/5-lucene4-9-learning-record-lucene-analysis-tokenizer/
4.純淨天空代碼範例: https://vimsky.com/zh-tw/examples/detail/csharp-ex-Lucene.Net.Documents-Document---class.html
More...
梨子, 2022/2/24 下午 08:23:46
Blog 待修改項目
1. 建立 API Server
     .Net 6.0
     .Cross Site
     .無 Session
     .評分機制

2. 前端修改
    . 改寫內容頁及編輯頁
    . 使用 Vue 及 API
    . 增加評分功能
    . 上傳圖片問題處理
    . 搜尋改用 Google Site Sarch

 
More...
Bike, 2022/1/11 下午 12:05:09
Visual Studio 2019安裝篇
Visual Studio 2019安裝篇
1. 主程式
https://visualstudio.microsoft.com/zh-hant/vs/?rr=https%3A%2F%2Fwww.google.com%2F

2. SVN套件
https://marketplace.visualstudio.com/items?itemName=VisualSVNLimited.VisualSVN-VS2019

3. .Net 放置到IIS要的額外設定
https://blog.johnwu.cc/article/iis-run-asp-net-core.html
More...
choco, 2019/10/17 下午 06:33:49
Windows 2016, IIS 安裝 Web Deploy 套件
好像為了這個找了很多次 google, 每次都花了不少時間..

1. 新增功能角色
 
2. 新增管理服務
 
3. 安裝 Web Deploy 套件 
https://www.iis.net/downloads/microsoft/web-deploy

4. 完工..

參考: https://stackoverflow.com/questions/41386690/missing-import-web-application-option-in-web-deploy-3-6
More...
Bike, 2019/5/22 下午 07:47:55
使用IntersectionObserver API 實作 Lazy load
過去都是使用 jquery lazy load js 來實作延遲載圖,
其運作原理是偵測scroll事件以及img物件的相對位置來決定要不要load圖片

現在有更簡單的方式,就是用 IntersectionObserver API 這個 Web標準
來偵測 img 物件是不是進入可視 webview 範圍
https://developers.google.com/web/updates/2016/04/intersectionobserver

document.addEventListener("DOMContentLoaded", function() {
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));;

if ("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
     entries.forEach(function(entry) {
        if (entry.isIntersecting) {
         let lazyImage = entry.target;
         lazyImage.src = lazyImage.dataset.src;
         lazyImage.srcset = lazyImage.dataset.srcset;
         lazyImage.classList.remove("lazy");
         lazyImageObserver.unobserve(lazyImage);
        }
     });
    });

    lazyImages.forEach(function(lazyImage) {
     lazyImageObserver.observe(lazyImage);
    });
}
//這裡可以加上else處理萬一瀏覽器不支援的狀況
});


網頁裡面只要用  <img data-src='圖片網址' class='lazy' />  就可以啦

IntersectionObserver API
目前 Can I Use 網站顯示大部分 browser 都支援,iOS safari 要 12.2 版才支援
應該可以放心使用


 
More...
darren, 2019/4/3 下午 07:48:07
|< 12 >|
頁數 1 / 2 下一頁
~ Uwinfo ~