مطالب
مشکل در جابجایی پروژه های svn
بنا به دلایلی، کدهای نگهداری شده توسط svn در سیستم خودم رو باید به سیستم دیگری وارد می‌کردم که شامل چند پروژه می‌شد و در قسمت مربوطه چند پروژه کار شده بود. تا این که مشکلی پیش آمد و جهت ادامه توسعه مجبور به برگرداندن پروژه‌ها به سیستم قبلی شدیم. حالا مشکل این بود که به repository قبلی هیچ گونه دسترسی نداشتیم و نمیخواستیم که کدهای در دست هم بصورت یک پروژه تازه وارد سرور svn شود. برای همین مجبور بودیم که از طریق relocate کردن، این کار را انجام دهیم، که به خطای زیر برخورد کردم:

بله؛ مشکل از اینجا ناشی می‌شد که سرور دوم کدها را import نکرده بود و بلکه با یک کامیت ساده، کدها وارد سرور شده بودند.
راه حل: در کل برای تغییر این uid که در تصویر مشخص شده، باید فایل SQLite مربوطه را یافت و به راحتی، دو مشخصه مربوط به آدرس مخزن و uid را در آن اصلاح کرد.
در پوشه svn کنار پروژه، فایلی با نام wc.db از نوع SQLite وجود دارد:

که به سادگی می‌توان آن‌را با برنامه‌ای مانند افزونه ای در فایرفاکس گشود و ویرایش کرد:


  همانطور که مشخص است، در جدول repository، تک ردیفی وجود دارد که مشخصات مربوط به مخزن svn در آن ذخیره شده است:


نکته: در این نوع آدرس دهی، باید حتما به نوع قرار گیری پروژه‌ها در کنار هم نیز دقت داشت. مثلا به محل آخری که در آدرس وجود دارد باید دقت کرد تا در مسیردهی، اشتباهی صورت نگیرد و فایلها جابجا کامیت نشوند.
مطالب
به دست آوردن اطلاعات کد اجراکننده یک متد
در C# 5 به بعد می‌توان به پارامترهای یک متد، پارامترهای دلخواهی را افزود تا به واسطه آن‌ها مشخصات کدی که این متد را فراخوانده، به دست آورد. روش انجام این کار، افزودن صفات زیر به پارامترهای متد مورد نظر است:
  1. [CallerFilePath]:مسیر کد فراخواننده را نگه می‌دارد.
  2. [CallerLineNumber]: شماره خط کد فراخواننده را  نگه می‌دارد.
  3. [CallerMemberName] : نام کد فراخوان را نگه می‌دارد .
این صفات کامپایلر را قادر می‌سازد که اطلاعاتی درباره فراخواننده متد مورد نظر، جمع‌آوری کند 
مثال زیر را در نظر بگیرید:
using System;
using System.Runtime.CompilerServices; 
namespace ConsoleApplication8
{ 
  class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.Read();
        }
        static void Test(
                [CallerMemberName] string memberName = null,
                [CallerFilePath] string filePath = null,
                [CallerLineNumber] int lineNumber = 0)
        {
            Console.WriteLine(memberName);
            Console.WriteLine(filePath);
            Console.WriteLine(lineNumber);
        }
    }
}
که نتیجه اجرای کد فوق به صورت زیر است:
Main          
c:\Pojects\ConsoleApplication8\Program.cs       
9                
 که عبارت Main عنوان متدی است که محل فراخوانی متد مورد نظر ماست و خط دوم حاوی مسیری است که کد فراخواننده متد مورد نظر ما درآن‌جا ذخیره شده است و عدد 9 نشانگر شماره خط محل فراخوانی متد Test است.
مطالب دوره‌ها
گزارش درصد پیشرفت عملیات در اعمال غیرهمزمان
گزارش درصد پیشرفت عملیات در اعمال طولانی، امکان لغو هوشمندانه‌تری را برای کاربر فراهم می‌کند. در دات نت 4.5 دو روش برای گزارش درصد پیشرفت عملیات اعمال غیرهمزمان تدارک دیده شده‌اند:
- اینترفیس جنریک IProgress واقع در فضای نام System
- کلاس جنریک Progress واقع در فضای نام System

در اینجا وهله‌ی از پیاده سازی اینترفیس IProgress به Task ارسال می‌شود. در این بین، عملیات در حال انجام با فراخوانی متد Report آن می‌تواند در هر زمانیکه نیاز باشد، درصد پیشرفت کار را گزارش کند.
namespace System
{
  public interface IProgress<in T>
  {
      void Report( T value );
  }
}
البته برای اینکه کار تعریف و پیاده سازی اینترفیس IProgress اندکی کاهش یابد، کلاس توکار Progress برای اینکار تدارک دیده شده‌است. نکته‌ی مهم آن استفاده از Synchronization Context برای ارائه گزارش پیشرفت در ترد UI است تا به سادگی بتوان از نتایج دریافتی، در رابط کاربری استفاده کرد.
namespace System
{
  public class Progress<T> : IProgress<T>
  {
    public Progress();
    public Progress( Action<T> handler );
    protected virtual void OnReport( T value );
  }
}


یک مثال از گزارش درصد پیشرفت عملیات به همراه پشتیبانی از لغو آن

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Async09
{
    public class TestProgress
    {
        public async Task DoProcessingReportProgress()
        {
            var progress = new Progress<int>(percent =>
            {
                Console.WriteLine(percent + "%");
            });

            var cts = new CancellationTokenSource();

            // call some where cts.Cancel();

            try
            {
                await doProcessing(progress, cts.Token);
            }
            catch (OperationCanceledException ex)
            {
                //todo: handle cancellations
                Console.WriteLine(ex);
            }

            Console.WriteLine("Done!");
        }

        private static async Task doProcessing(IProgress<int> progress, CancellationToken ct)
        {
            await Task.Run(async () =>
            {
                for (var i = 0; i != 100; ++i)
                {
                    await Task.Delay(100, ct);
                    if (progress != null)
                        progress.Report(i);

                    ct.ThrowIfCancellationRequested();
                }
            }, ct);
        }
    }
}
متد private static async Task doProcessing طوری طراحی شده‌است که از مفاهیم لغو یک عملیات غیرهمزمان و همچنین گزارش درصد پیشرفت آن توسط اینترفیس IProgress پشتیبانی می‌کند. در اینجا هر زمانیکه نیاز به گزارش درصد پیشرفت باشد، متد Report وهله‌ی ارسالی به آرگومان progress فراخوانی خواهد شد.
برای تدارک این وهله، از کلاس توکار Progress دات نت در متد public async Task DoProcessingReportProgress استفاده شده‌است.
این متد جنریک بوده و برای مثال نوع آن در اینجا int تعریف شده‌است. سازنده‌ی آن می‌تواند یک callback را قبول کند. هر زمانیکه متد Report در متد doProcessing فراخوانی گردد، این callback در سمت کدهای استفاده کننده، فراخوانی خواهد شد. مثلا توسط مقدار آن می‌توان یک Progress bar را نمایش داد.
به علاوه روش دیگری را در مورد لغو یک عملیات در اینجا ملاحظه می‌کنید. متد ThrowIfCancellationRequested نیز سبب خاتمه‌ی عملیات می‌گردد؛ البته اگر در کدهای برنامه در جایی متد Cancel توکن، فراخوانی گردد. برای مثال یک دکمه‌ی لغو عملیات در صفحه قرارگیرد و کار آن صرفا فراخوانی cts.Cancel باشد.
مطالب
کوئری نویسی در EF Core - قسمت پنجم - اعمال تجمعی - بخش اول
امکان انجام محاسبات تجمعی، یکی از مواردی است که قدرت بانک‌های اطلاعاتی رابطه‌ای را نمایش می‌دهد. توسط این نوع کوئری‌ها از محدوده‌ی CRUD (ثبت/ویرایش/به روز رسانی) خارج شده و وارد دنیای تصمیم‌گیری‌ها می‌شویم. تعداد مثال‌های اعمال تجمعی این سری قابل توجه‌است. به همین جهت در دو قسمت ارائه می‌شوند.


مثال 1: چه تعداد امکانات، توسط این مجموعه ارائه می‌شود؟
var count = context.Facilities.Count();
برای شمارش ساده‌ی تعداد ردیف‌های یک کوئری، از متد Count استفاده می‌شود که به صورت زیر ترجمه خواهد شد:



مثال 2: چه تعداد امکانات گران قیمتی توسط این مجموعه ارائه می‌شود؟
می‌خواهیم تعداد امکاناتی را بیابیم که guestcost آن‌ها بزرگتر یا مساوی 10 است.
var count = context.Facilities.Count(x => x.GuestCost >= 10);
این گزارش نسبت به مثال قبلی، یک Where را بیشتر دارد که می‌توان این شرط را در همان متد Count نیز ذکر کرد؛ با این خروجی نهایی:



مثال 3: هر کاربر چه تعدادی کاربر دیگری را توصیه کرده‌است؟
خروجی این گزارش بر اساس recommendedby و count باشد و مرتب شده‌ی بر اساس ID افراد.
var members = context.Members
                        .Where(member => member.RecommendedBy != null)
                        .GroupBy(member => member.RecommendedBy)
                        .Select(group => new
                        {
                            RecommendedBy = group.Key,
                            Count = group.Count()
                        })
                        .OrderBy(result => result.RecommendedBy)
                        .ToList();
در دو مثال قبل، تنها یک حاصل عددی را از گزارشات دریافت کردیم. اما در اینجا نیاز است به ازای هر شخص، تعداد توصیه شده‌های او محاسبه شوند. به همین جهت نیاز است اطلاعات را به ازای هر شخص مجزا، گروه بندی کرد و سپس متد Count را بر روی این گروه ویژه اعمال نمود و همینطور برای مابقی گروه‌ها. در اینجا GroupBy بر روی خاصیت RecommendedBy انجام شده‌است. این خاصیت در Select بعدی، همان group.Key استفاده شده‌است.



مثال 4: تعداد slots رزرو شد‌ه‌ی به ازای هر کدام از امکانات موجود را نمایش دهید.
جهت یادآوری از قسمت اول: «هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعت‌های رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته می‌شود و زمان شروع این رزرو نیز ثبت می‌شود.»
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شده‌ی بر اساس ID هر امکان موجود.
var facilities = context.Bookings
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.FacId)
                                    .ToList();
در این گزارش بجای استفاده از متد Count، از متد Sum استفاده شده‌است. چون می‌خواهیم جمع slots را به ازای هر امکان موجود محاسبه کنیم، ابتدا گروه‌های مجزای این امکانات را تشکیل می‌دهیم و سپس Sum را به هر گروهی، به صورت مجزایی اعمال می‌کنیم. در اینجا group.Key دقیقا همان booking.FacId است و در متد Sum، امکان دسترسی به خواص booking نیز وجود دارد.



مثال 5: تعداد slots رزرو شد‌ه‌ی در ماه September 2012 را به ازای هر کدام از امکانات موجود، نمایش دهید.
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شده‌ی بر اساس ID تعداد slots.
var date1 = new DateTime(2012, 09, 01);
var date2 = new DateTime(2012, 10, 01);

var facilities = context.Bookings
                                    .Where(booking => booking.StartTime >= date1
                                                        && booking.StartTime < date2)
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.TotalSlots)
                                    .ToList();
این گزارش تنها یک قسمت Where را نسبت به گزارش قبلی بیشتر دارد و این Where باید دقیقا پیش‌از گروه بندی اطلاعات اعمال شود. به عبارتی ابتدا باید ردیف‌های اصلی گزارش مشخص باشند تا بتوان آن‌ها را گروه بندی کرد.



مثال 6: محاسبه کنید در سال 2012 و به ازای هر ماه مجزای آن، چه تعداد slots رزرو شده‌اند.
خروجی این گزارش بر اساس facid, month, Total Slots باشد و مرتب شده‌ی بر اساس ID و شماره‌ی ماه.
var date1 = new DateTime(2012, 01, 01);
var date2 = new DateTime(2013, 01, 01);

var facilities = context.Bookings
                                    .Where(booking => booking.StartTime >= date1
                                                        && booking.StartTime < date2)
                                    .GroupBy(booking => new { booking.FacId, booking.StartTime.Month })
                                    .Select(group => new
                                    {
                                        group.Key.FacId,
                                        group.Key.Month,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.FacId)
                                        .ThenBy(result => result.Month)
                                    .ToList();
دو نکته‌ی جدید در این گزارش نسبت به مثال‌های قبلی وجود دارند:
الف) می‌توان گروه بندی را بر روی بیش از یک ستون اعمال کرد. در این حالت در Select بعدی، group.Key به کل شیء گروه بندی شده‌، اشاره می‌کند.
ب) روش انتخاب ماه میلادی از یک خاصیت DateTime و گروه بندی بر اساس آن

که به صورت زیر ترجمه می‌شود:



مثال 7: چه تعداد کاربر مجموعه، حداقل یکبار امکاناتی را رزرو کرده‌اند؟

var count = context.Bookings.Select(booking => booking.MemId).Distinct().Count();
هدف از این گزارش، رسیدن به COUNT DISTINCT است.
- (*)COUNT یعنی بازگشت تعداد ردیف‌های نهایی گزارش.
- COUNT(address) یعنی بازگشت تعداد آدرس‌های غیرنال، در کل ردیف‌های نهایی گزارش.
- COUNT(DISTINCT address) یعنی بازگشت تعداد آدرس‌های غیرمشابه در کل ردیف‌های نهایی گزارش.

  COUNT DISTINCT را EF-Core به صورت ترکیبی از یک sub-query ترجمه می‌کند:



مثال 8: امکاناتی را لیست کنید که بیش از 1000 slots رزرو شده دارند.
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شده‌ی بر اساس ID هر امکان موجود.
var facilities = context.Bookings
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .Where(result => result.TotalSlots > 1000)
                                    .OrderBy(result => result.FacId)
                                    .ToList();
در مثال‌های قبل، از Where جهت تشکیل تعداد ردیف‌های اصلی گزارش و سپس گروه بندی آن‌ها استفاده کردیم. در اینجا می‌خواهیم Where را بر روی نتیجه‌ی حاصل از گروه بندی اعمال کنیم. در کوئری LINQ فوق، خواص قابل دسترسی پس از Select نهایی، همان‌هایی هستند که توسط آن ارائه می‌شوند. این نوع Whereها در SQL حاصل به Having ترجمه خواهند شد:



مثال 9: میزان فروش کل هر امکان موجود را محاسبه کنید.
خروجی این گزارش بر اساس name, revenue باشد و مرتب شده‌ی بر اساس میزان فروش. بخاطر داشته باشید که میزان فروش کاربران ثبت نام شده با کاربران مهمان یکی نیست.
 var facilities =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.Name)
                                .Select(group => new
                                {
                                    Name = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                })
                                .OrderBy(result => result.TotalRevenue)
                                .ToList();
همانند تمام گروه بندی‌ها، ابتدا باید ردیف‌های اصلی گزارش را تشکیل داد و سپس بر روی آن‌ها گروه بندی نهایی را اعمال نمود. به همین جهت در ابتدا خاصیت محاسباتی Revenue را بر اساس کاربران مهمان با ID مساوی صفر و کاربران اصلی مجموعه، تشکیل داده و گروه بندی را به نام هر مجموعه اعمال می‌کنیم. سپس جمع Revenue محاسبه شده را به ازای هر گروه محاسبه کرده و نتیجه را بازگشت می‌دهیم.



مثال 10: کدامیک از امکانات موجود، میزان فروشی کمتر از 1000 داشته‌اند؟
خروجی این گزارش بر اساس name, revenue باشد و مرتب شده‌ی بر اساس میزان فروش. بخاطر داشته باشید که میزان فروش کاربران ثبت نام شده با کاربران مهمان یکی نیست.
var facilities =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.Name)
                                .Select(group => new
                                {
                                    Name = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                })
                                .Where(result => result.TotalRevenue < 1000)
                                .OrderBy(result => result.TotalRevenue)
                                .ToList();
این مورد نیز همانند گزارش 9 است که یک Where به نتیجه‌ی حاصل از آن اعمال شده که در خروجی نهایی به Having ترجمه می‌شود:



مثال 11: کدامیک از امکانات موجود، بیشترین slots رزرو شده را دارد؟

var item = context.Bookings
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderByDescending(result => result.TotalSlots)
                                    .FirstOrDefault();
ساده‌ترین روش حل این مساله، گروه بندی اطلاعات بر اساس هر امکان موجود و سپس محاسبه‌ی TotalSlots هرکدام، به صورت مجزایی است. در ادامه ردیف‌های حاصل را بر اساس TotalSlots محاسبه شده، به صورت نزولی مرتب می‌کنیم. اولین ردیفی که در بالای گزارش قرار می‌گیرد همان FacId ای است که بیشترین TotalSlots را دارد.



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
کار با Docker بر روی ویندوز - قسمت پنجم - ایجاد Imageهای سفارشی
تا اینجا با نحوه‌ی اجرای برنامه‌های مختلف توسط داکر مانند وب سرور لینوکسی nginx و یا IIS ویندوزی آشنا شدیم؛ اما هنوز محتوایی را در آن‌ها هاست نکرده‌ایم. در این قسمت این موضوع را بررسی خواهیم کرد و در طی این فرآیند، با نحوه‌ی ساخت Imageهای سفارشی نیز آشنا خواهیم شد.


روش نگاشت محتوای یک سایت استاتیک در یک Container که وب سرور است

فرض کنید یک سایت استاتیک بوت استرپی را تهیه کرده‌اید و قصد دارید آن‌را توسط وب سرور nginx، هاست کنید. برای این‌کار، چندین گزینه پیش روی ما هستند:
گزینه‌ی اول: دریافت image مربوط به nginx، سپس ایجاد یک container از آن و در آخر با استفاده از «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» که در قسمت قبل بررسی کردیم، این وب سایت را آماده‌ی اجرا و دسترسی می‌کنیم.
گزینه‌ی دوم: کپی کردن فایل‌های وب سایت از سیستم میزبان، به درون فایل سیستم خود container.
گزینه‌ی سوم: ایجاد یک image سفارشی که از ابتدا به همراه فایل‌های وب سایت استاتیک ما است و در این حالت تنها کافی است این image را تبدیل به container اجرایی کنیم.


روش اول: به اشتراک گذاری فایل سیستم میزبان با کانتینر وب سرور جهت هاست آن

در قسمت قبل، یک فایل tar ایجاد شده‌ی در سیستم میزبان ویندوزی را با یک کانتینر لینوکسی به اشتراک گذاشتیم تا بتوانیم محتویات آن‌را استخراج کنیم. در اینجا قصد داریم پوشه‌ی وب سایت استاتیک خود را که در سیستم میزبان ویندوزی قرار دارد، با وب سرور nginx که توسط یک container در حال اجرا است، به اشتراک بگذاریم تا آن‌را هاست کند.
فرض کنید وب سایت استاتیک ما در مسیر c:\users\vahid\mysite سیستم میزبان قرار دارد که داخل آن یک فایل index.html و تعدادی فایل css و js آماده‌ی برای هاست شدن، وجود دارند. برای هاست آن توسط nginx، از دستور زیر استفاده خواهیم کرد:
 docker run --rm -it -p 8080:80 -v c:\users\vahid\mysite:/usr/share/nginx/html nginx
در این دستور:
- سوئیچ rm سبب می‌شود تا پس از خاتمه‌ی کار nginx، این container نیز حذف شود.
- از سوئیچ it استفاده شده‌است تا با فشردن ctrl+c، بتوانیم پروسه‌ی container را خاتمه دهیم و پس از آن، برنامه‌ی nginx دیگر در background در حال اجرا نباشد (اجرای آن در foreground).
- سپس پورت 8080 سیستم میزبان، به پورت 80 وب سرور nginx نگاشت شده‌است. چون containerها دارای network stack خاص خودشان هستند (که آن‌را در قسمت سوم بررسی کردیم)، پورت 80 آن‌ها با پورت 80 سیستم میزبان تداخل نمی‌کند و اگر برای مثال بر روی پورت 80 سیستم جاری، IIS در حال اجرا باشد، سبب عدم اجرا شدن وب سرور nginx به دلیل تداخل پورت‌ها نمی‌شود.
- در ادامه روش volume mount را مشاهده می‌کنید که در قسمت قبل بررسی کردیم. مسیر c:\users\vahid\mysite سیستم میزبان، به مسیر ویژه‌ی /usr/share/nginx/html داخل container نگاشت شده‌است. این مسیر، یک مسیر استاندارد بوده و در مستندات docker hub این وب سرور، ذکر شده‌است.
- در آخر هم نام image این وب سرور را ذکر کرده‌ایم.

پس از اجرای این دستور، اگر nginx پیش‌تر دریافت نشده باشد، image آن دریافت شده، یک container بر اساس آن ساخته می‌شود و سپس با پارامترهایی که توضیح دادیم، اجرا خواهد شد. اکنون اگر در سیستم میزبان، مسیر http://localhost:8080 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد.


روش دوم: کپی کردن فایل‌های وب سایت از سیستم میزبان، به درون فایل سیستم خود container

همانطور که در قسمت سوم نیز بررسی کردیم، فایل سیستم مربوط به هاست، به طور کامل از فایل سیستم container، جدا و ایزوله است و بدون volume mount، یک container نمی‌تواند به فایل‌های میزبان خود دسترسی پیدا کند. بنابراین گزینه‌ی دیگری که در اینجا وجود خواهد داشت، کپی کردن فایل‌های میزبان و انتقال آن‌ها به container می‌باشد؛ شبیه به کپی کردن فایل‌ها از یک کامپیوتر موجود در شبکه به کامپیوتر دیگری در آن.
برای این منظور ابتدا nginx را در پس‌زمینه اجرا می‌کنیم:
 docker run -d -p 8080:80 --name nginx nginx
در این دستور، سوئیچ‌های rm و it حذف شده‌اند. علت اینجا است که سوئیچ d، سبب اجرای این دستور در پس‌زمینه می‌شود؛ یعنی بلافاصله سبب بازگشت ما به خط فرمان خواهد شد و در این حالت نمی‌خواهیم که این container حذف شود. همچنین یک نام نیز به آن انتساب داده شده‌است تا بتوان ساده‌تر با آن کار کرد.
پس از اجرای این دستور و بازگشت به command prompt، جهت اطمینان حاصل کردن از اجرای آن در پس زمینه، دستور docker ps را صادر می‌کنیم که لیست آن، حاوی گزارشی از container‌های در حال اجرا است.
اکنون توسط دستور ویژه‌ی docker exec، می‌خواهیم درون یک container در حال اجرا، پروسه‌ای را اجرا کنیم. یعنی با اینکه پروسه‌ی nginx داخل این container در حال اجرا است، برای مثال می‌خواهیم یک shell را نیز داخل آن اجرا کنیم:
 docker exec -it nginx bash
در اینجا دستور docker exec، سبب اجرای bash shell داخل کانتینری با نام nginx می‌شود (همان سوئیچ name در دستور قبلی و نه نام image آن) و چون می‌خواهیم به این shell در foreground دسترسی داشته باشیم، از سوئیچ it نیز استفاده شده‌است. پس از اجرا شدن bash shell، اکنون به فایل سیستم این container دسترسی یافته‌ایم. برای مثال دستور ls را صادر کنید تا لیستی از آن‌را مشاهده نمائید. سپس به کمک آن، به پوشه‌ی ویژه‌ی html این وب سرور وارد می‌شویم:
 cd /usr/share/nginx/html
و برای مثال می‌توان در آن تغییر ایجاد کرد:
ls
mv index.html index2.html
exit
این دستورات سبب می‌شوند تا فایل پیش‌فرض index.html آن، به index2.html تغییر نام یابد و سپس از این shell خارج می‌شویم و به shell سیستم میزبان باز خواهیم گشت. در اینجا دستور docker cp (که در PowerShell سیستم میزبان اجرا می‌شود)، امکان کپی کردن فایل‌ها را از سیستم میزبان به یک container میسر می‌کند.
 docker cp c:\users\vahid\mysite nginx:/usr/share/nginx/html
پس از دستور docker cp ابتدا مسیر مبداء مشخص می‌شود و سپس ابتدا نام container مقصد به همراه یک : و در ادامه مسیر مقصد نهایی کپی در آن container ذکر خواهند شد. به این ترتیب فایل‌های وب سایت استاتیک ما در سیستم میزبان به پوشه‌ی html مخصوص nginx، در کانیتنری که در حال اجرای آن است کپی می‌شوند. برای آزمایش صحت این کپی می‌توان دستور زیر را صادر کرد که لیست فایل‌های این پوشه‌ی html را نمایش می‌دهد:
 docker exec nginx ls /usr/share/nginx/html
اینبار نیز اگر در سیستم میزبان، مسیر http://localhost:8080 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد که فایل‌های آن از داخل خود container تامین می‌شوند و وابستگی به سیستم میزبان ندارند.


روش سوم: ایجاد یک image سفارشی که از ابتدا به همراه فایل‌های وب سایت استاتیک ما است

در روش دوم، موفق شدیم که فایل‌های مدنظر خود را به درون container در حال اجرا کپی کنیم. اکنون می‌خواهیم یک snapshot را از آن تهیه کنیم؛ شبیه به کاری که با ماشین‌های مجازی نیز انجام می‌شود و این روشی است که از آن برای ساخت یک image سفارشی استفاده می‌شود. برای این منظور از دستور docker commit استفاده می‌شود تا تصویری را از وضعیت یک container در حال اجرا، در آن لحظه تهیه کنیم:
 docker commit nginx mysite:nginx
پس از دستور docker commit، نام container ای که می‌خواهیم تصویر وضعیت جاری آن‌را ذخیره کنیم، ذکر می‌شود. پس از آن به صورت اختیاری می‌توان یک نام جدید و همچنین tag ای را برای آن ذکر کرد.
اکنون پس از اجرای این دستور، با استفاده از فرمان docker images می‌توان مشاهده کرد که image جدید mysite، با tag ای معادل nginx، ایجاد شده‌است.
در ادامه برای اجرای این image جدید، می‌توان از دستور زیر استفاده کرد:
 docker run -d -p 8090:80 --name mysite mysite:nginx
روش اجرای آن همانند سایر imageهای موجود است و در اینجا از نام image به همراه tag آن استفاده شده‌است. همچنین پورت نگاشت شده‌ی آن‌را به سیستم میزبان نیز 8090 انتخاب کرده‌ایم. نامی را نیز به آن نسبت داده‌ایم تا بتوان از آن در دستور docker exec استفاده کرد.
اکنون اگر در سیستم میزبان، مسیر http://localhost:8090 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد و یا توسط دستور زیر می‌توانید فایل‌های موجود در پوشه‌ی html وب سرور nginx این container جدید در حال اجرا را ملاحظه نمائید:
 docker exec mysite ls /usr/share/nginx/html
که این فایل‌ها نه از طریق نگاشت فایل سیستم میزبان، به مسیری در container جاری تامین شده‌اند و نه از جائی به داخل آن کپی شده‌اند. بلکه دقیقا از image از پیش آماده شده‌ی آن خوانده شده‌اند.


نگاهی به لایه‌های یک Image در مقایسه با یک Container

زمانیکه خواستیم image جدید و سفارشی خاص خود را ایجاد کنیم، با image اصلی nginx شروع کردیم. اولین لایه‌ی موجود در این image، سیستم عاملی است که می‌تواند آن‌را اجرا کند. برفراز این لایه، لایه‌ی خود nginx قرار گرفته‌است. اگر خواستید تاریخچه‌ی ایجاد یک image را مشاهده کنید، از دستور docker history nginx استفاده نمائید. خروجی آن لیست دستوراتی را نمایش می‌دهد که برای ساخت این image مورد استفاده قرار گرفته‌اند. البته دستور docker history nginx --no-trunc، اطلاعات بیشتری را با نمایش لیست کامل و خلاصه نشده‌ی دستورات، ارائه می‌دهد. این دستورات را در صفحه‌ی docker hub هر image نیز می‌توان مشاهده کرد. در قسمت full description هر image، در ابتدای توضیحات، قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک فایل که دقیقا همین دستورات را لیست کرده‌است. به این فایل، docker file گفته می‌شود که روش ساخت یک image را توضیح می‌دهد. هدف آن، خودکار سازی اجرای دستوراتی است که سبب ساخت یک image می‌شوند.

در ادامه اگر از این image، یک container را ایجاد کنیم، این container هر دو لایه‌ی OS و Framework را به همراه خواهد داشت؛ به علاوه‌ی لایه‌ی دیگری به نام Container/Run که می‌توان فایل‌های آن‌را خواند و یا در آن نوشت. بنابراین لایه‌ای که فایل‌های وب سایت استاتیک ما در آن کپی شدند، دقیقا همین لایه‌است.


و زمانیکه از یک container تصویری تهیه می‌شود، تغییراتی را که به فایل سیستم آن ایجاد کرده‌ایم، به صورت یک لایه‌ی جدید بر روی لایه‌های قبلی آن image، ظاهر و ثبت می‌شود. برای اثبات این موضوع، می‌توان از دستور docker diff nginx استفاده کرد. در اینجا nginx نام container ای است که می‌خواهیم تغییرات آن‌را با image قبلی که بر پایه‌ی آن ایجاد شده‌است، مشاهده کنیم.


تبدیل دستورات docker به یک docker file

تا اینجا یک چنین دستوراتی را برای اجرای کانتینر nginx، کپی فایل‌ها به آن و سپس تهیه‌ی یک تصویر از آن، اجرا کردیم:
docker run -d -p 8080:80 --name nginx nginx
docker cp c:\users\vahid\mysite nginx:/usr/share/nginx/html
docker commit nginx mysite:nginx
برای خودکار سازی آن‌ها هرچند می‌توان این دستورات را در یک اسکریپت نیز قرار داد، اما docker، قابلیت پردازش اسکریپت‌های خاص خود را نیز دارد که به آن Dockerfile گفته می‌شود. برای این منظور سطرهای فوق به صورت زیر تغییر می‌کنند:
بجای سطر اول، تنها نام image ای را که می‌خواهیم کار را بر مبنای آن انجام دهیم، ذکر می‌کنیم:
 FROM nginx
دستور دوم نیز تبدیل به دستور کپی Docker می‌شود:
 COPY mysite /usr/share/nginx/html
این دو سطر را به صورت یک فایل متنی، با نام ویژه‌ی Dockerfile ذخیره می‌کنیم (بدون پسوند) و این Dockerfile را دقیقا در کنار پوشه‌ی mysite قرار می‌دهیم (داخل پوشه‌ی c:\users\vahid) تا کار کپی را از همینجا شروع کند.
سپس برای اجرای این فایل، بجای دستور docker commit آخر، از دستور زیر استفاده می‌کنیم:
 docker build -f Dockerfile -t mysite:nginx-df .
البته می‌توان f Dockerfile- را نیز از این دستور حذف کرد؛ چون مقدار پیش‌فرض آن است (مگر آنکه بخواهیم مسیر خاصی را دقیقا مشخص کنیم):
 docker build -t mysite:nginx-df .
در هر دو دستور آخری که ذکر شدند، در انتهای دستور، یک نقطه نیز قرار دارد که به آن build context گفته می‌شود؛ یا دقیقا همین پوشه‌ای که در آن قرار داریم (c:\users\vahid).
تگ این image را نیز متفاوت با قبلی‌ها انتخاب کرده‌ایم؛ nginx-df بجای مقدار قبلی.
در این حالت اگر دستور آخر را اجرا کنیم، دستور docker images گزارش اضافه شدن این image جدید را ارائه خواهد داد.

مرجع کامل ساخت Dockerfileها را در اینجا می‌توانید مطالعه کنید.


ساخت یک image سفارشی برای هاست یک وب سایت استاتیک در IIS

تا اینجا از وب سرور لینوکسی nginx برای هاست وب سایت استاتیک خود استفاده کردیم. در ادامه می‌خواهیم از وب سرور IIS برای اینکار استفاده نمائیم. بنابراین ابتدا نیاز است یا از ویندوز سرور استفاده کنیم و یا می‌توان با کلیک راست بر روی آیکن Docker در قسمت Tray Icons ویندوز، به Windows Containers سوئیچ کرد و سپس به صورت زیر عمل نمود.
اینبار محتوای Dockerfile ای که کنار پوشه‌ی mysite قرار می‌گیرد، به صورت زیر خواهد بود:
FROM microsoft/iis:nanoserver

COPY mysite c:/inetpub/wwwroot
کار با image اصلی iis با tag مخصوص nanoserver که کم حجم‌تر است، شروع می‌شود. سپس فایل‌های mysite به پوشه‌ی wwwroot این وب سرور کپی خواهد شد.
در ادامه با استفاده از دستور زیر و اجرای فایل Dockerfile، این image جدید را با tag ای به نام iis ایجاد می‌کنیم:
 docker build -t mysite:iis .
پس از آن دستورات docker images و docker ps را جهت مشاهده‌ی وضعیت این image جدید اجرا کنید.


به اشتراک گذاری imageهای سفارشی در Docker Hub

برای به اشتراک گذاری imageهای سفارشی خود در Docker Hub، نیاز است tag آن‌ها را توسط دستور docker tag مطابق فرمت ویژه‌ی docker hub ویرایش کرد:
 docker tag mysite:nginx-df my_user_name/some_name:new_tag_name
در این دستور، Tag فعلی، با ذکر نام کاربری، نام مخزنی جدید در docker hub و سپس یک tag دلخواه، ویرایش می‌شود.
و در آخر برای انتشار آن می‌توان از دستور docker push استفاده کرد:
 docker push my_user_name/some_name:new_tag_name
اگر در اینجا پیام خطای unauthorized را مشاهده کردید، ابتدا دستور docker login را اجرا کنید تا بتوانید به سایت docker hub لاگین کنید (بر اساس مشخصات اکانت خود در داکر هاب) و سپس دستور فوق را اجرا نمائید.
پس از پایان کار اگر به سایت docker hub و مخازن خود مراجعه کنید، این image جدید قابل مشاهده خواهد بود.
مطالب
کار با Docker بر روی ویندوز - قسمت ششم - کار با بانک‌های اطلاعاتی درون Containerها
تا اینجا نحوه‌ی اجرای برنامه‌ها را داخل کانتینرها بررسی کردیم؛ اما هنوز در مورد داده‌های آن‌ها بحث نکرده‌ایم. اگر بانک‌های اطلاعاتی را به درون کانتینرها منتقل کنیم، چه بر سر داده‌های آن‌ها می‌آید؟


بررسی روش اجرای MS SQL Server Express درون یک Container

اگر مخازن Imageهای رسمی مایکروسافت را در داکرهاب بررسی کنیم، به مخازنی مانند mssql-server-windows-express ، mssql-server و یا mssql-server-linux نیز خواهیم رسید. در اینجا آخرین نگارش Image مربوط به SQL Server Express آن، حدود 7GB حجم دارد. برای دریافت آن ابتدا به Windows Containers سوئیچ کنید و سپس دستور زیر را صادر نمائید:
 docker pull microsoft/mssql-server-windows-express
پس از دریافت آن، اگر به مستندات رسمی آن در داکر هاب مراجعه کنیم، دستوری را به صورت زیر برای اجرای آن عنوان کرده‌است:
 docker run -d -p 1433:1433 -e sa_password=<SA_PASSWORD> -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
در این دستور:
- سوئیچ d سبب می‌شود تا پس از اجرای این دستور، بلافاصله به command prompt بازگشت داده شویم و SQL Server Express در background اجرا شود.
- سپس پورت 1433 میزبان به پورت 1433 کانتینر، نگاشت شده‌است که پورت استاندارد SQL Server است.
- سوئیچ e، امکان تنظیم متغیرهای محیطی را میسر می‌کند؛ برای مثال ورود کلمه‌ی عبور کاربر SA و یا پذیرش مجوز آن. برای نمونه، این کلمه‌ی عبور را مساوی password وارد کنید؛ هرچند کار نخواهد کرد، اما بررسی خطاهای به همراه آن مفید است.
- و در آخر نام image مرتبط ذکر شده‌است.

پس از اجرای این دستور، کانتینر SQL Server Express، در پس زمینه شروع به کار خواهد کرد و بلافاصله به خط فرمان بازگشت داده می‌شویم. در اینجا ممکن است آغاز SQL Server اندکی طول بکشد. برای اینکه دریابیم در این لحظه وضعیت پروسه‌ی آن به چه صورتی است، دستور docker logs id را صادر کنید. پس از آن خطایی مانند password validation failed را مشاهده خواهیم کرد. عنوان می‌کند که پیچیدگی کلمه‌ی عبور وارد شده کافی نیست.

یک نکته: زمانیکه دستور docker run را اجرا می‌کنیم، یک هش طولانی را نمایش می‌دهد و پس از آن به خط فرمان بازگشت داده می‌شویم. این هش طولانی، همان id کانتینر در حال اجرا است. برای مثال در دستور docker logs id می‌توان 3 حرف ابتدای این هش را بجای id وارد کرد. البته این id را توسط دستور docker ps نیز می‌توان بدست آورد.

بنابراین با توجه به اینکه دستور docker logs id، خطایی را گزارش کرده‌است، توسط دستور docker stop id، این کانتینر را متوقف کرده و آن‌را مجددا با کلمه‌ی عبوری مانند pass!w0rd1 اجرا می‌کنیم:
 docker run -d -p 1433:1433 -e sa_password=pass!w0rd1 -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
اینبار نیز مجددا دستور docker logs id را بر اساس id جدید این کانتینر اجرا می‌کنیم که پیام Started SQL Server را نمایش می‌دهد. بنابراین تا به اینجا موفق شدیم پروسه‌ی SQL Server Express را بدون مشکلی آغاز کنیم.


همانطور که در قسمت سوم نیز عنوان شد، اگر این کانتینر را بر روی ویندوز سرور، در حالت Windows Containers اجرا کنیم (و نه در حالت Hyper-V)، پروسه‌های اجرای شده‌ی داخل یک Container را می‌توان با Job Object Idهای یکسانی که دارند، در Task Manager ویندوز، در کنار سایر پروسه‌های سیستم، شناسایی کرد.


اتصال به SQL Server Express اجرا شده‌ی داخل یک Container توسط SQL Server Management Studio

پس از اجرای SQL Server Express دخل کانتینر، مطابق تنظیمات آن، چه در سیستم میزبان و چه در داخل کانتینر، به پورت 1433 گوش فرا داده می‌شود. به همین منظور نیاز است IP این کانتینر را نیز بدست آوریم. برای اینکار دستور ipconfig را در سیستم میزبان صادر کنید تا بر اساس مشخصات کارت شبکه‌ی مجازی آن، بتوان IP آن‌را بدست آورد (دستور docker inspect id نیز چنین اطلاعاتی را به همراه دارد). اکنون می‌توان از داخل سیستم راه دور دیگری که SQL Server Management Studio بر روی آن نصب است، توسط این IP و پورت، به SQL Server Express متصل شد.


البته در اینجا نیازی به ذکر پورت نیست؛ چون پورت 1433، شماره پورت پیش‌فرض است. بعد از اتصال، می‌توان کارهای متداولی مانند ایجاد یک بانک اطلاعاتی جدید را انجام داد.
برای آزمایش، یکبار دستور docker ps را صادر کنید تا id این کانتینر مشخص شود. سپس دستور docker stop id را صادر کنید تا پروسه SQL Server Express خاتمه یابد. اکنون اگر در SQL Server Management Studio قصد کار با آن‌را داشته باشیم، پیام عدم اتصال مشاهده می‌شود. اکنون برای اجرای مجدد کانتینر، دستور docker start id را صادر کنید.


بررسی روش اجرای MySQL داخل یک Container

برای اجرای MySQL نیاز است به Linux Containers سوئیچ کنیم. حجم tag ویژه‌ی latest آن نیز حدود 138MB است که نسبت به SQL Server Express هفت گیگابایتی، بسیار کمتر است!
در همان صفحه‌ی مستندات آن در داکرهاب، دستور اجرایی آن نیز ذکر شده‌است:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
در اینجا نیز توسط سوئیچ e که مخفف environment است، یکسری از متغیرهای محیطی MySQL، مانند کلمه‌ی عبور آن قابل تنظیم هستند. همچنین سوئیچ d نیز برای اجرای آن در پس زمینه، ذکر شده‌است. همین دستور را به همین شکل، صرفا با حذف tag آن، جهت اشاره‌ی به آخرین نگارش موجود این image، اجرا می‌کنیم:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
با اجرای این دستور، در ابتدا MySQL از داکرهاب دریافت شده و سپس در پس زمینه اجرا خواهد شد. پیش از بازگشت به command prompt، یک هش طولانی نیز نمایش داده می‌شود که همان id کانتینر در حال اجرای آن است. برای اینکه بتوانیم ریز جزئیات رخ داده را بهتر مشاهده کنیم، می‌توان از دستور docker logs id استفاده کرد.

یک نکته: می‌توان یک command prompt جدید را باز کرد و سپس دستور docker logs -f id را در آن صادر کرد. به این صورت لاگ‌های لحظه‌ای یک کانتینر را نیز می‌توان مشاهده کرد (f در اینجا به معنای follow است).

اکنون می‌خواهیم MySQL Client موجود در همین Container در حال اجرا را، اجرا کنیم (اجرای پروسه‌ای درون یک کانتینر در حال اجرا). برای اینکار از دستور docker exec استفاده می‌شود:
docker ps
docker exec -it id mysql --user=root --password=my-secret-pw
ابتدا توسط دستور docker ps مقدار id این کانتینر را بدست می‌آوریم و سپس در دستور بعدی، از آن استفاده خواهیم کرد.
در اینجا توسط دستور docker exec ابتدا یک interactive shell را درخواست کرده‌ایم (اجرای foreground یک برنامه‌ی شل). سپس id این کانتینر باید ذکر شود. پس از آن نام فایل اجرایی MySQL Client قید شده و در پایان، نام کاربری و کلمه‌ی عبور اتصال به آن که در دستور docker run تنظیم شده‌اند، ذکر می‌شوند.
با اجرای این دستور، به خط فرمان MySQL Client داخل این کانتینر دسترسی پیدا می‌کنیم. در اینجا می‌توان دستورات مختلفی را برای کار با پروسه‌ی mysql اجرا کرد؛ مانند اجرای دستور show databases که لیست بانک‌های اطلاعاتی موجود را نمایش می‌دهد:
mysql> show databases;
use mysql;
show tables;
select * from user;
exit;


روش مدیریت داده‌های بانک‌های اطلاعاتی توسط Docker

در قسمت قبل دریافتیم که لایه‌ی رویی یک container، دارای قابلیت read/write است و برای مثال می‌توان فایل‌های یک وب سایت استاتیک را در آنجا کپی و سپس هاست کرد. اما این لایه، لایه‌ی مناسبی برای ذخیره سازی داده‌های یک بانک اطلاعاتی نیست. در اینجا برای مدیریت بهتر این نوع داده‌ها، از مفهومی به نام volume استفاده می‌شود.
برای درک روش مدیریت داده‌ها توسط داکر، دستور docker volume ls را اجرا کنید. مشاهده خواهید کرد که docker یک volume پیش‌فرض را نیز ایجاد کرده‌است. البته با volumes پیشتر در قسمت چهارم، در بخش «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» نیز آشنا شده‌ایم. این volume پیش‌فرض، کار ذخیره سازی اطلاعات را حتی اگر کانتینری در حال اجرا نباشد نیز انجام می‌دهد. وجود یک چنین قابلیتی جهت از دست نرفتن اطلاعات ارزشمند ذخیره شده‌ی در بانک‌های اطلاعاتی بسیار ضروری است.
البته لازم به ذکر است، این volume ای را که در اینجا مشاهده می‌کنید، توسط Dockerfile خود mysql به صورت خودکار ایجاد می‌شود. برای مثال در داکرهاب، در قسمت full description این image، در ابتدای توضیحات قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک Dockerfile. اگر یکی از آن‌ها را باز کنید، چنین سطری را در آن مشاهده خواهید کرد:
  VOLUME /var/lib/mysql
این دستور سبب می‌شود چنین مسیری (مسیر پیش‌فرض ثبت اطلاعات mysql) به صورت یک volume جدید، خارج از فایل سیستم کانتینر، بر روی سیستم میزبان، ایجاد شود. سپس این مسیر و volume جدید، توسط داکر به صورت خودکار به این کانتینر mount خواهد شد و برای این موارد نیازی نیست کار خاصی توسط ما انجام شود.
اینکار نه فقط برای بالابردن کارآیی اعمال read/write انجام شده‌ی توسط container انجام می‌شود، بلکه حتی اگر این کانتینر را توسط دستور docker rm id حذف کنیم، دستور docker volume ls، هنوز همان volume ای را که در حین نصب mysql به صورت خودکار ایجاد شده بود، نمایش می‌دهد. علت اینجا است که طول عمر این volume، وابسته‌ی به طول عمر کانتینر آن نیست. به این ترتیب حذف تصادفی یک کانتینر، سبب از دست رفتن اطلاعات ارزشمند داخل بانک اطلاعاتی آن نمی‌شود.


روش تعیین صریح یک volume برای یک کانتینر بانک اطلاعاتی، توسط volumeهای نامدار

دستور docker run ای را که برای اجرای mysql صادر کردیم، یک volume خودکار را ایجاد کرده‌است و اگر آن‌را با دستور docker volume ls بررسی کنیم، دارای یک نام هش مانند است که به آن anonymous volume هم گفته می‌شود. در ادامه قصد داریم یک volume نامدار را ایجاد کنیم و سپس از آن جهت ذخیره سازی اطلاعات چندین وهله از کانتینر mysql استفاده نمائیم.
پیش از ادامه بحث، ابتدا توسط دستور docker rm id، کانتینر mysql ای را که پیشتر ایجاد کردیم حذف کنید؛ هرچند این دستور، volume متناظر با آن‌را حذف نمی‌کند.
سپس برای اینکه یک کانتینر جدید mysql را با ذکر صریح volume آن ایجاد و اجرا کنیم، می‌توان از دستور زیر استفاده کرد:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
در اینجا از سوئیچ v برای ایجاد یک volume نامدار استفاده شده‌است و در آن بجای ذکر قسمت مسیر پوشه‌ای در سمت میزبان، صرفا یک نام، مانند db، پیش از ذکر : قید شده‌است. پس از :، مسیری که این volume قرار است در آن کانتینر به آن نگاشت شود، ذکر شده‌است.
اکنون اگر دستور docker volume ls را صادر کنیم، در لیست خروجی آن، نام db قابل مشاهده‌است.

در ادامه پروسه‌ی MySQL Client داخل این کانتینر را اجرا کرده:
 docker exec -it some-mysql mysql --user=root --password=my-secret-pw
و تغییراتی را به صورت زیر اعمال می‌کنیم:
mysql> show databases;
create database pets;
show databases;
exit;
در اینجا بانک اطلاعاتی جدید pets ایجاد شده‌است.

اکنون در ابتدا این کانتینر را متوقف کرده و سپس آن‌را حذف می‌کنیم:
docker ps
docker stop id
docker rm id
هرچند اگر دستور حذف را با سوئیچ f- نیز اجرا کنیم (به معنای force)، کار stop را به صورت خودکار انجام می‌دهد.

در ادامه مجددا همان دستور قبلی را که توسط آن volume نامداری، ایجاد کردیم، اجرا می‌کنیم:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
اینبار اگر دستور docker volume ls را مجددا صادر کنیم، مشاهده خواهیم کرد این کانتینر جدید، بجای ایجاد یک volume جدید، از همان volume موجود db که آن‌را پیشتر ایجاد کردیم، استفاده می‌کند؛ هرچند کانتینری که آن‌را ایجاد کرده‌است، دیگر وجود خارجی ندارد. در این حالت اگر MySQL Client این کانتینر را اجرا نمائیم:
 docker exec -it some-mysql mysql --user=root --password=my-secret-pw
و سپس دستور نمایش بانک‌های اطلاعاتی آن‌را صادر کنیم:
 mysql> show databases;
در خروجی آن هنوز بانک اطلاعاتی pets که پیشتر ایجاد شده بود، قابل مشاهده‌است. بنابراین حذف و یا ایجاد کانتینرها، تاثیری را بر روی volumeهای ایجاد شده، نخواهند داشت.


روش حذف volumes اضافی

با توجه به اینکه volumeها، طول عمر متفاوتی را نسبت به کانتینرها دارند، ممکن است پس از مدتی فضای دیسک سخت شما را پر کنند. برای مثال به ازای هربار اجرای دستور docker run مربوط با MYSQL با نامی متفاوت، یک volume جدید نیز ایجاد می‌شود.
خروجی دستور docker inspect id به همراه قسمتی است به نام mounts که خاصیت name آن، دقیقا مساوی نام volume متناظر با کانتینر بررسی شده‌است. همچنین خاصیت source آن، محل دقیق ذخیره سازی این volume را بر روی فایل سیستم میزبان مشخص می‌کند.
برای حذف آن‌ها، ابتدا نیاز است کانتینرها را متوقف کرد. دستور زیر تمام کانتینرهای در حال اجرا را متوقف می‌کند. در اینجا دستور docker ps -q، لیست id تمام کانتینرهای در حال اجرا را باز می‌گرداند (در این دستورات، افزودن پارامتر q، سبب بازگشت صرفا idها می‌شود):
 docker stop $(docker ps -q)
اگر می‌خواهید تمام کانتینرهای موجود را حذف کنید:
 docker rm $(docker ps -aq)
و یا دستور زیر ابتدا تمام کانتینرهای موجود را متوقف کرده و سپس آن‌ها را حذف می‌کند:
 docker rm -f $(docker ps -aq)
دستور زیر تمام volumes موجود را حذف می‌کند:
 docker volume rm $(docker volume ls -q)
دستور زیر یک کانتینر با id مشخص شده را به همراه volume نامگذاری نشده‌ی مرتبط با آن، متوقف و سپس حذف می‌کند:
 docker rm -fv id
دستور زیر، لیست تمام volumes غیراستفاده شده‌ی توسط کانتینرهای موجود را نمایش می‌دهد (به یک چنین volumeهای در اینجا dangling گفته می‌شود؛ volume ای که کانتینر آن حذف شده‌است):
 docker volume ls -f dangling=true
 که می‌تواند لیست مناسبی برای حذف باشند:
 docker volume rm $(docker volume ls -qf dangling=true)
مطالب
SQL Injection چیست؟
برای ایجاد امنیت در نرم افزار، باید ابتدا مشکلات رایج را بدانیم. یکی از رایجترین نقائص امنیتی نرم افزارها SQL Injection می‌باشد.
SQL Injection در لغت به معنی تزریق کد SQL می‌باشد. در اصلاح یعنی تزریق دستوراتی به کد SQL تولیدی یک نرم افزار به نحوی که به جای عمل مورد انتظار برنامه نویس آن، کاری را که ما می‌خواهیم انجام دهد. مثلا به جای اینکه هنگام ورود به برنامه وقتی کاربر مشخصات کاربری خود را وارد می‌کند، مشخصات کاربری را به نحوی وارد کنیم که بتوانیم بعنوان مدیر سامانه و یا یک کاربر معمولی بدون داشتن کلمه عبور وارد سیستم شویم.
البته همیشه از این نوع حمله برای ورود به سیستم استفاده نمی‌شود. یعنی ممکن است هکر به عنوان یک کاربر عادی وارد سیستم شود ولی با به کاربردن دستورات خاص SQL در بخش‌های مختلف، بتواند اطلاعاتی را حذف نماید.
خوب حالا این کار چگونه انجام می‌شود؟
فرض کنید برنامه نویسی کد چک نام کاربری را اینگونه نوشته باشد:
SqlCommand cmd=new SqlCommand ("select count(*) from login where user='"+userName+"' and pass='"+password+"'",con);

فکر نکنید خوب این نوع کد نویسی مربوط به زمان تیرکمون شاه است! همین امروز در نظارت از یک پروژه به این نکته برخورد کردم! دلیل نوشتن این مقاله هم همین کد بود.
خوب حالا مگر کد بالا چه مشکلی دارد؟ ;) اگر کاربر در نامه کاربری و کلمه عبور مقادیر معمولی وارد کند (مانند admin, salam123) کد sql تولید شده به شکل زیر خواهد بود:

select count(*) from login where user='admin' and pass='salam123'

خوب حالا اگر کاربر کمی با ورودی‌ها بازی کند. به عنوان مثال فرض کنید به جای کلمه عبور تایپ کند
' or 1=1 --

نتیجه حاصله خواهد بود:
select count(*) from login where user='admin' and pass='' or 1=1 --'

با وارد کردن این دستور کاربر بدون داشتن کلمه عبور خواهد توانست وارد سیستم شود. موردی که توضیح دادم پایه مسئله بود. ما قصد آموزش هک نداریم ولی داشتن اطلاعات پایه لازم است. ممکن است فردی بگوید خوب ما قبل از تولید همچین کدی ' را از رشته کلمه عبور حذف می‌کنیم. خیلی خوب ولی اگر هکر از معادل unicode آن استفاده کرد چه؟ اگر و اگر و اگر...
راه حل‌های متعددی برای این موضوع پیشنهاد شده است. ولی ساده‌ترین و کارآمد‌ترین راه، استفاده از پارامترها می‌باشد که علاوه بر حذف این خطر باعث ایجاد و ذخیره query plan در sql server می‌شود و اجرای این query را در آینده تسریع می‌کند.
بنابراین می‌توان کد فوق را به صورت زیر بازنویسی کرد:
SqlCommand cmd=new SqlCommand ("select count(*) from login where user=@u and pass=@p",con);

cmd.Parameters.Add("@u", SqlDbType.Varchar, 10).Value=TextLogin.Text.Trim();

cmd.Parameters.Add("@p", SqlDbType.Varchar,10).Value=TextPwd.Text.Trim();
مطالب
معرفی یک ابزار گزارشگیری رایگان مخصوص WPF

تا صحبت از گزارشگیری به میان بیاید احتمالا معرفی ابزارهای تجاری مانند Reporting services ، کریستال ریپورت، stimulsoft.com ، fast-report.com و امثال آن درصدر لیست توصیه کنندگان و مشاوران قرار خواهند داشت. اما خوب برای ایجاد یک گزارشگیری ساده حتما نیازی نیست تا به این نوع ابزارهای تجاری مراجعه کرد. ابزار رایگان و سورس باز جالبی هم در این باره جهت پروژه‌های WPF در دسترس است:



در ادامه در طی یک مثال قصد داریم از این کتابخانه استفاده کنیم:

1) تنظیم وابستگی‌ها
پس از دریافت کتابخانه فوق، ارجاعات زیر باید به پروژه شما اضافه شوند:
CodeReason.Reports.dll (از پروژه فوق) و ReachFramework.dll (جزو اسمبلی‌های استاندارد دات نت است)

2) تهیه منبع داده‌ گزارش
کتابخانه‌ی فوق به صورت پیش فرض با DataTable‌ کار می‌کند. بنابراین کوئری‌های شما یا باید خروجی DataTable داشته باشد یا باید از یک سری extension methods برای تبدیل IEnumerable به DataTable استفاده کرد (در پروژه پیوست شده در پایان مطلب، این موارد موجود است).
برای مثال فرض کنید می‌خواهیم رکوردهایی را از نوع کلاس Product زیر در گزارش نمایش دهیم:

namespace WpfRptTests.Model
{
public class Product
{
public string Name { set; get; }
public int Price { set; get; }
}
}
3) تعریف گزارش
الف) اضافه کردن فایل تشکیل دهنده ساختار و ظاهر گزارش
گزارش‌‌های این کتابخانه مبتنی است بر اشیاء FlowDocument استاندارد WPF . بنابراین از منوی پروژه گزینه‌ی Add new item در قسمت WPF آن یک FlowDocument جدید را به پروژه اضافه کنید ( باید دقت داشت که Build action این فایل باید به Content تنظیم گردد). ساختار ابتدایی این FlowDocument به صورت زیر خواهد بود که به آن FlowDirection و FontFamily مناسب جهت گزارشات فارسی اضافه شده است. همچنین فضای نام مربوط به کتابخانه‌ی گزارشگیری CodeReason.Reports نیز باید اضافه گردد.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft" FontFamily="Tahoma"
xmlns:xrd="clr-namespace:CodeReason.Reports.Document;assembly=CodeReason.Reports"
PageHeight="29.7cm" PageWidth="21cm" ColumnWidth="21cm">

</FlowDocument>

مواردی که در ادامه ذکر خواهند شد محتوای این گزارش را تشکیل می‌دهند:
ب) مشخص سازی خواص گزارش

<xrd:ReportProperties>
<xrd:ReportProperties.ReportName>SimpleReport</xrd:ReportProperties.ReportName>
<xrd:ReportProperties.ReportTitle>گزارش از محصولات</xrd:ReportProperties.ReportTitle>
</xrd:ReportProperties>
در اینجا ReportName و ReportTitle باید مقدار دهی شوند (دو dependency property که در کتابخانه‌ی CodeReason.Reports تعریف شده‌اند)

ج) مشخص سازی Page Header و Page Footer
اگر می‌خواهید عباراتی در بالا و پایین تمام صفحات گزارش تکرار شوند می‌توان از SectionReportHeader و SectionReportFooter این کتابخانه به صورت زیر استفاده کرد:
    <xrd:SectionReportHeader PageHeaderHeight="2" Padding="10,10,10,0" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportHeader>

<xrd:SectionReportFooter PageFooterHeight="2" Padding="10,0,10,10" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
نام کاربر:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
صفحه
<xrd:InlineContextValue PropertyName="PageNumber" FontWeight="Bold" /> از
<xrd:InlineContextValue PropertyName="PageCount" FontWeight="Bold" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportFooter>

دو نکته در اینجا حائز اهمیت هستند: xrd:InlineDocumentValue و xrd:InlineContextValue
InlineDocumentValue را می‌توان در کد‌های برنامه به صورت سفارشی اضافه کرد. بنابراین هر جایی که نیاز بود مقدار ثابتی از طریق کد نویسی به گزارش تزریق و اضافه شود می‌توان از InlineDocumentValue استفاده کرد. برای مثال در کدهای ViewModel برنامه که در ادامه ذکر خواهد شد دو مقدار PrintDate و RptBy به صورت زیر تعریف و مقدار دهی شده‌اند:
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");
برای مشاهده مقادیر مجاز مربوط به InlineContextValue به فایل ReportContextValueType.cs سورس کتابخانه مراجعه کنید که شامل PageNumber, PageCount, ReportName, ReportTitle است و توسط CodeReason.Reports به صورت پویا تنظیم خواهد شد.

د) مشخص سازی ساختار تولیدی گزارش

<Section Padding="80,10,40,10" FontSize="12">
<Paragraph FontSize="24" TextAlignment="Center" FontWeight="Bold">
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
<Paragraph TextAlignment="Center">
گزارش از لیست محصولات در تاریخ:
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
توسط:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
<xrd:SectionDataGroup DataGroupName="ItemList">
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="0.02cm">
<Table.Resources>
<!-- Style for header/footer rows. -->
<Style x:Key="headerFooterRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Background" Value="LightGray"/>
</Style>

<!-- Style for data rows. -->
<Style x:Key="dataRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontSize" Value="12"/>
</Style>

<!-- Style for data cells. -->
<Style TargetType="{x:Type TableCell}">
<Setter Property="Padding" Value="0.1cm"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.01cm"/>
</Style>
</Table.Resources>

<Table.Columns>
<TableColumn Width="0.8*" />
<TableColumn Width="0.2*" />
</Table.Columns>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>نام محصول</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>قیمت</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

<TableRowGroup Style="{StaticResource dataRowStyle}">
<xrd:TableRowForDataTable TableName="Product">
<TableCell>
<Paragraph>
<xrd:InlineTableCellValue PropertyName="Name" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<xrd:InlineTableCellValue PropertyName="Price" AggregateGroup="Group1" />
</Paragraph>
</TableCell>
</xrd:TableRowForDataTable>
</TableRowGroup>

<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Right">
<Bold>جمع کل</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" />
</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

</Table>

<Paragraph TextAlignment="Center" Margin="5">
در این گزارش
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Count"
EmptyValue="هیچ"
FontWeight="Bold" /> محصول با جمع کل قیمت
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" /> وجود دارند.
</Paragraph>
</xrd:SectionDataGroup>
</Section>
برای اینکه بتوان این قسمت‌ها را بهتر توضیح داد، نیاز است تا تصاویر مربوط به خروجی این گزارش نیز ارائه شوند:




در ابتدا توسط دو پاراگراف، عنوان گزارش و یک سطر زیر آن نمایش داده شده‌اند. بدیهی است هر نوع شیء و فرمت مجاز در FlowDocument را می‌توان در این قسمت نیز قرار داد.
سپس یک SectionDataGroup جهت نمایش لیست آیتم‌ها اضافه شده و داخل آن یک جدول که بیانگر ساختار جدول نمایش رکوردهای گزارش می‌باشد، ایجاد گردیده است.
سه TableRowGroup در این جدول تعریف شده‌اند.
TableRowGroup های اولی و آخری دو سطر اول و آخر جدول گزارش را مشخص می‌کنند (سطر عناوین ستون‌ها در ابتدا و سطر جمع کل در پایان گزارش)
از TableRowGroup میانی برای نمایش رکوردهای مرتبط با نام جدول مورد گزارشگیری استفاده شده است. توسط TableRowForDataTable آن نام این جدول باید مشخص شود که در اینجا همان نام کلاس مدل برنامه است. به کمک InlineTableCellValue، خاصیت‌هایی از این کلاس را که نیاز است در گزارش حضور داشته باشند، ذکر خواهیم کرد. نکته‌ی مهم آن AggregateGroup ذکر شده است. توسط آن می‌توان اعمال جمع، محاسبه تعداد، حداقل و حداکثر و امثال آن‌را که در فایل InlineAggregateValue.cs سورس کتابخانه ذکر شده‌اند، به فیلدهای مورد نظر اعمال کرد. برای مثال می‌خواهیم جمع کل قیمت را در پایان گزارش نمایش دهیم به همین جهت نیاز بود تا یک AggregateGroup را برای این منظور تعریف کنیم.
از این AggregateGroup در سومین TableRowGroup تعریف شده به کمک xrd:InlineAggregateValue جهت نمایش جمع نهایی استفاده شده است.
همچنین اگر نیاز بود در پایان گزارش اطلاعات بیشتری نیز نمایش داده شود به سادگی می‌توان با تعریف یک پاراگراف جدید، اطلاعات مورد نظر را نمایش داد.

4) نمایش گزارش تهیه شده
نمایش این گزارش بسیار ساده است. View برنامه به صورت زیر خواهد بود:
<Window x:Class="WpfRptTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeReason.Reports.Controls;assembly=CodeReason.Reports"
xmlns:vm="clr-namespace:WpfRptTests.ViewModel"
Title="MainWindow" WindowState="Maximized" Height="350" Width="525">
<Window.Resources>
<vm:ProductViewModel x:Key="vmProductViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmProductViewModel}}">
<c:BusyDecorator IsBusyIndicatorHidden="{Binding RptGuiModel.IsBusyIndicatorHidden}">
<DocumentViewer Document="{Binding RptGuiModel.Document}" />
</c:BusyDecorator>
</Grid>
</Window>

تعریف ابتدایی RptGuiModel به صورت زیر است (جهت مشخص سازی مقادیر IsBusyIndicatorHidden و Document در حین بایندینگ اطلاعات):

using System.ComponentModel;
using System.Windows.Documents;

namespace WpfRptTests.Model
{
public class RptGuiModel
{
public IDocumentPaginatorSource Document { get; set; }
public bool IsBusyIndicatorHidden { get; set; }
}
}
و این View اطلاعات خود را از ViewModel زیر دریافت خواهد نمود:
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using CodeReason.Reports;
using WpfRptTests.Helper;
using WpfRptTests.Model;

namespace WpfRptTests.ViewModel
{
public class ProductViewModel
{
#region Constructors (1)

public ProductViewModel()
{
RptGuiModel = new RptGuiModel();
if (Stat.IsInDesignMode) return;
//انجام عملیات نمایش گزارش در یک ترد دیگر جهت قفل نشدن ترد اصلی برنامه
showReportAsync();
}

#endregion Constructors

#region Properties (1)

public RptGuiModel RptGuiModel { set; get; }

#endregion Properties

#region Methods (3)

// Private Methods (3)

private static List<Product> getProducts()
{
var products = new List<Product>();
for (var i = 0; i < 100; i++)
products.Add(new Product { Name = string.Format("Product{0}", i), Price = i });

return products;
}

private void showReport()
{
try
{
//Show BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = false;

var reportDocument =
new ReportDocument
{
XamlData = File.ReadAllText(@"Report\SimpleReport.xaml"),
XamlImagePath = Path.Combine(Environment.CurrentDirectory, @"Report\")
};

var data = new ReportData();

// تعریف متغیرهای دلخواه و مقدار دهی آن‌ها
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");

// استفاده از یک سری اطلاعات آزمایشی به عنوان منبع داده
data.DataTables.Add(getProducts().ToDataTable());

var xps = reportDocument.CreateXpsDocument(data);
//انقیاد آن به صورت غیر همزمان در ترد اصلی برنامه
DispatcherHelper.DispatchAction(
() => RptGuiModel.Document = xps.GetFixedDocumentSequence()
);
}
catch (Exception ex)
{
//وجود این مورد ضروری است زیرا بروز استثناء در یک ترد به معنای خاتمه آنی برنامه است
//todo: log errors
}
finally
{
//Hide BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = true;
}
}

private void showReportAsync()
{
var thread = new Thread(showReport);
thread.SetApartmentState(ApartmentState.STA); //for DocumentViewer
thread.Start();
}

#endregion Methods
}
}

توضیحات:
برای اینکه حین نمایش گزارش، ترد اصلی برنامه قفل نشود، از ترد استفاده شد و استفاده ترد به همراه DocumentViewer کمی نکته دار است:
- ترد تعریف شده باید از نوع STA باشد که در متد showReportAsync مشخص شده است.
- حین بایندیگ Document تولید شده توسط کتابخانه‌ی گزارشگیری به خاصیت Document کنترل، حتما باید کل عملیات در ترد اصلی برنامه صورت گیرد که سورس کلاس DispatcherHelper را در فایل پیوست خواهید یافت.

کل عملیات این ViewModel در متد showReport رخ می‌دهد، ابتدا فایل گزارش بارگذاری می‌شود، سپس متغیرهای سفارشی مورد نظر تعریف و مقدار دهی خواهند شد. در ادامه یک سری داده آزمایشی تولید و به DataTables گزارش ساز اضافه می‌شوند. در پایان XPS Document متناظر آن تولید شده و به کنترل نمایشی برنامه بایند خواهد شد.

دریافت سورس این مثال

مطالب
آشنایی با چالش های امنیتی در توسعه برنامه‌های تحت وب، بخش اول
در پروژه‌های بزرگ نرم افزاری، از قدیم بحث تامین امنیت پروژه، یکی از چالش‌های مهم بوده است. از دیدگاه شخصی بنده، یک مدیر نرم افزار یا حتی یک توسعه دهنده‌ی برنامه‌های تحت وب، لازم است علاوه بر صرف وقت مطالعاتی و آشنایی و تسلط بر مباحث طراحی معماری سیستم‌های تحت وب، که از اهمیت بالا و مقیاس بزرگی برخوردارند آشنایی لازم را با چالش‌های امنیتی در پیاده سازی اینگونه سیستم‌ها داشته باشد. امنیت در یک سیستم بزرگ و ارائه دهنده خدمات، باعث می‌شود تا کاربر علاوه بر یک تجربه کاربری (user experience) خوب از سیستم که حاصل پیاده سازی صحیح سیستم می‌باشد، اعتماد ویژه‌ای به سیستم مذکور داشته باشد. گاها کاربران به علت بی اعتمادی به شرایط امنیتی حاکم بر یک سیستم، از تجربه کاربری خوب یک سیستم چشم پوشی می‌کنند. اهمیت این مسئله تا جاییست که غول‌های تکنولوژی دنیا همچون Google درگیر این چالش می‌باشند و همیشه سعی بر تامین امنیت کاربران علاوه بر ایجاد تجربه کاربری خوب دارند. پس عدم توجه به این موضوع میتواند خسارات وارده جبران ناپذیری را به یک سیستم از جهت‌های مختلف وارد کند.

در این سری از مقالات، بنده سعی دارم تا حد توان در رابطه با چالش‌های امنیتی موجود در زمینه توسعه برنامه‌های تحت وب، مطالبی را منتشر کنم. از این رو امیدوارم تا این سری از مقالات برای دوستان مفید واقع گردد.

در این سری از مقالات چالش‌های امنیتی زیر مورد بحث و بررسی واقع خواهند گردید 

XSS , LDAPi ,RFI ,LFI ,SQLi ,RFD ,LFD ,SOF ,BSQLI ,DNN ,BOF ,CRLF ,CSRF ,SSI ,PCI ,SCD ,AFD ,RCE

در بخش اول از این سری مقالات ، به بررسی آسیب پذیری Cross-site scripting میپردازیم .

واژه XSS مخفف Cross-site scripting، نوعی از آسیب پذیریست که در برنامه‌های تحت وب نمود پیدا میکند. به طور کلی و خلاصه، این آسیب پذیری به فرد نفوذ کننده اجازه تزریق اسکریپت‌هایی را به صفحات وب، می‌دهد که در سمت کاربر اجرا می‌شوند ( Client Side scripts ) . در نهایت این اسکریپت‌ها توسط سایر افرادی که از صفحات مورد هدف قرار گرفته بازدید می‌کنند اجرا خواهد شد.

هدف از این نوع حمله :

بدست آوردن اطلاعات کوکی‌ها و سشن‌های کاربران ( مرتبط با آدرسی که صفحه آلوده شده در آن قرار دارد ) است. سپس فرد نفوذ کننده متناسب با اطلاعات بدست آمده می‌تواند به اکانت شخصی کاربران مورد هدف قرار گرفته، نفوذ کرده و از اطلاعات شخصی آن‌ها سوء استفاده کند .

به صورت کلی دو طبقه بندی برای انواع حملات Cross-site scripting وجود دارند.

حملات XSS ذخیره سازی شده ( Stored XSS Attacks ) :

در این نوع ، کدهای مخرب تزریق شده، در سرور سایت قربانی ذخیره میشوند. محل ذخیره سازی می‌تواند دیتابیس سایت یا هر جای دیگری که داده‌ها توسط سایت یا برنامه تحت وب بازیابی می‌شوند و نمایش داده می‌شوند باشد. اما اینکه چگونه کدهای مخرب در منابع یاد شده ذخیره میشوند؟

فرض کنید در سایت جاری آسیب پذیری مذکور وجود دارد. راه‌های ارسال داده‌ها به این سایت چیست؟ نویسندگان میتوانند مطلب ارسال کنند و کاربران میتوانند نظر دهند. حال اگر در یکی از این دو بخش بررسی‌های لازم جهت مقابله با این آسیب پذیری وجود نداشته باشد و نوشته‌های کاربران که می‌تواند شامل کدهای مخرب باشد مستقیما در دیتابیس ذخیره شده و بدون هیچ اعتبار سنجی نمایش داده شود چه اتفاقی رخ خواهد داد؟ مسلما با بازدید صفحه آلوده شده، کدهای مخرب بر روی مرورگر شما اجرا و کوکی‌های سایت جاری که متعلق به شما هستند برای هکر ارسال میشود و ...

حملات XSS منعکس شده ( Reflected XSS Attacks ) :

در این نوع از حمله، هیچ نوع کد مخربی در منابع ذخیره سازی وبسایت یا اپلیکیشن تحت وب توسط فرد مهاجم ذخیره نمی‌شود ! بلکه از ضعف امنیتی بخش‌هایی همچون بخش جستجو وب سایت، بخش‌های نمایش پیغام خطا و ... استفاده میشود ... اما به چه صورت؟

در بسیاری از سایت‌ها، انجمن‌ها و سیستم‌های سازمانی تحت وب، مشاهده می‌شود که مثلا در بخش جستجو، یک فیلد برای وارد کردن عبارت جستجو وجود دارد. پس از وارد کردن عبارت جستجو و submit فرم، علاوه بر نمایش نتایج جستجو، عبارت جستجو شده نیز به نمایش گذاشته میشود و بعضا در بسیاری از سیستم‌ها این عبارت قبل از نمایش اعتبار سنجی نمی‌شود که آیا شامل کدهای مخرب می‌باشد یا خیر. همین امر سبب میشود تا اگر عبارت جستجو شامل کدهای مخرب باشد، آن‌ها به همراه نتیجه‌ی جستجو اجرا شوند.

اما این موضوع چگونه مورد سوء استفاده قرار خواهد گرفت؟ مگر نه اینکه این عبارت ذخیره نمیشود پس با توضیحات فوق، کد فقط بر روی سیستم مهاجم که کد جستجو را ایجاد می‌کند اجرا می‌شود، درست است؟ بله درست است ولی به نقطه ضعف زیر توجه کنید ؟

www.test.com/search?q=PHNjcmlwdD5hbGVydChkb2N1bWVudC5jb29raWUpOzwvc2NyaXB0Pg==

این آدرس حاصل submit  شدن فرم جستجو وب‌سایت test (نام وب‌سایت واقعی نیست و برای مثال است )  و ارجاع به صفحه نتایج جستجو میباشد. در واقع این لینک برای جستجوی یک کلمه یا عبارت توسط این وبسایت تولید شده و از هر کجا به این لینک مراجعه کنید عبارت مورد نظر مورد جستجو واقع خواهد شد. در واقع عبارت جستجو به صورت Base64 به عنوان یک query String به وبسایت ارسال می‌شود؛ علاوه بر نمایش نتایج، عبارت جستجو شده نیز به کاربر نشان داده شده و اگر آسیب پذیری مورد بحث وجود داشته باشد و عبارت شامل کدهای مخرب باشد، کدهای مخرب بر روی مرورگر فردی که این لینک را باز کرده اجرا خواهد شد!

در این صورت کافیست فرد مهاجم لینک مخرب را به هر شکلی به فرد مورد هدف بدهد ( مثلا ایمیل و ... ). حال در صورتیکه فرد لینک را باز کند (با توجه به اینکه لینک مربوط به یک سایت معروف است و عدم آگاهی کاربر از آسیب پذیری موجود در لینک، باعث باز کردن لینک توسط کاربر می‌شود)، کدها بر روی مرورگرش اجرا شده و کوکی‌های سایت مذکور برای مهاجم ارسال خواهد شد ... به این نوع حمله XSS   ، نوع انعکاسی می‌گویند که کاملا از توضیحات فوق الذکر، دلیل این نامگذاری مشخص می‌باشد.

اهمیت مقابله با این حمله :

برای نمونه این نوع باگ حتی تا سال گذشته در سرویس ایمیل یاهو وجود داشت. به شکلی که یکی از افراد انجمن hackforums به صورت Private این باگ را به عنوان Yahoo 0-Day XSS Exploit در محیط زیر زمینی و بازار سیاه هکرها به مبلغ چند صد هزار دلار به فروش می‌رساند. کاربران مورد هدف کافی بود تا فقط یک ایمیل دریافتی از هکر را باز کنند تا کوکی‌های سایت یاهو برای هکر ارسال شده و دسترسی ایمیل‌های فرد قربانی برای هکر فراهم شود ... ( در حال حاظر این باگ در یاهو وجو ندارد ).

چگونگی جلوگیری از این آسیب پذیری

در این سری از مقالات کدهای پیرامون سرفصل‌ها و مثال‌ها با ASP.net تحت فریم ورک MVC و به زبان C# خواهند بود. هر چند کلیات مقابله با آسیب پذیری هایی از این دست در تمامی زبان‌ها و تکنولوژی‌های تحت وب یکسان میباشند.

خوشبختانه کتابخانه‌ای قدرتمند برای مقابله با حمله مورد بحث وجود دارد با نام AntiXSS که میتوانید آخرین نسخه آن را با فرمان زیر از طریق nugget به پروژه خود اضافه کنید. البته ذکر این نکته حائز اهمیت است که Asp.net و فریم ورک MVC به صورت توکار تا حدودی از بروز این حملات جلوگیری می‌کند. برای مثال به این صورت که در View ‌ها شما تا زمانی که از MvcHtmlString استفاده نکنید تمامی محتوای مورد نظر برای نمایش به صورت Encode شده رندر می‌شوند. این داستان برای Url ‌ها هم که به صورت پیش فرض encode میشوند صدق می‌کند. ولی گاها وقتی شما برای ورود اطلاعات مثلا از یک ادیتور WYSWYG استفاده می‌کنید و نیاز دارید داده‌ها را بدون encoding رندر کنید. آنگاه به ناچار مجاب بر اعمال یک سری سیاست‌های خاص‌تر بر روی داده مورد نظر برای رندر می‌شوید و نمی‌توانید از encoding توکار فوق الذکر استفاده کنید. آنگاه این کتابخانه در اعمال سیاست‌های جلوگیری از بروز این آسیب پذیری می‌تواند برای شما مفید واقع شود.

 PM> Install-Package AntiXSS
این کتابخانه مجموعه‌ای از توابع کد کردن عبارات است که از مواردی همچون Html, XML, Url, Form, LDAP, CSS, JScript and VBScript پشتیبانی می‌کند. استفاده از آن بسیار ساده می‌باشد. کافیست ارجاعات لازم را به پروژه خود افزوده و به شکل زیر از توابع ارائه شده توسط این کتابخانه استفاده کنید: 
…
var reviewContent = model.UserReview;
reviewContent = Microsoft.Security.Application.Encoder.HtmlEncode(review);
…

امیدوارم در اولین بخش از این سری مقالات، به صورت خلاصه مطالب مهمی که باعث ایجاد فهم کلی در رابطه با حملات Xss وجود دارد، برای دوستان روشن شده و پیش زمینه فکری برای مقابله با این دست از حملات برایتان به وجود آمده باشد. 

نظرات مطالب
ارسال عکس به stimulsoft و ایجاد گزارش
با سلام، من از این روش برای ارسال عکس به Bussiness Object استفاده کردم، که در واقع یک لیست رو که در برنامه C# ساخته شده و یک ستون عکس داره رو میخوام گزارش بگیرم، علاوه بر توضیحات شما از این مطلب هم استفاده کردم. اما ستون تصاویر نمایش داده نمیشه و یا اینکه به ازای type‌های مختلف عبارت هایی مثل عبارت زیر نمایش داده میشه:
System.Drawing.Bitmap
تصاویری که داشتم در دیتابس sql ذخیره شده  و من برای یک گزارش جدید دیگه نمیخوام توی sql إخیره کنم  و نیاز دارم که لیست رو مستقیما به گزارش ارسال کنم. ممنون میشم راهنمایی کنید.
private void ModifyItemList()
        {
            ItemList.Clear();
            var AllItems = from BetaData in BetaContext.BetaDatas
                           where BetaData.ProjectName == Globals.ProjectName &&
                           BetaData.ProjectCode == Globals.ProjectCode &&
                           (BetaData.Type == Globals.Types.Partition || BetaData.Type == Globals.Types.Door)
                           select BetaData;
                           
            foreach(BetaData data in AllItems)
            {
                byte[] imageByte = data.Image.ToArray();
                MemoryStream MS = new MemoryStream(imageByte);
                Image img = Image.FromStream(MS);
                StiImage stiImg = new StiImage();
                stiImg.Image = img;
                Items newItem = new Items
                {
                    Image = img,
                    Name = data.Name
                };
                ItemList.Add(newItem);
            }
                         
        }

        private void modiefieItemListToolStripMenuItem_Click(object sender, EventArgs e)
        {
            
        }

        private void modifiedItemListToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ModifyItemList();
            ModifiedItemListReport.RegBusinessObject("Items",ItemList);
            ModifiedItemListReport.Compile();
            ModifiedItemListReport.Render();
            ModifiedItemListReport.Show();
        }

        public class Items
        {
            public string Name { get; set; }
            public Image Image { get; set; }
        }