کتابخانه های استاندارد در Go (بخش دوم)




آنچه گذشت...

در پست پیشین به معرفی برخی از کتابخانه های استاندارد Go پرداختیم و گفتیم که مرور و مطالعه این کتابخانه ها به شما این امکان را می دهد که با نحوه پیاده سازی و ساختار زبان آشنا شده و چرخ های موجود را دوباره اختراع نکنید. در این پست با یک پروژه کوچک و به صورت کاربردی با کتابخانه های io، os، ioutils و http‍‍‍ آشنا می شویم. کدهای نهایی در گیب هاب من موجود هستند.

معرفی بسته های http و io

بسته io توابع و عملگرهای مربوط به ورودی/خروجی را پیاده سازی می کند. از آنجا که پیاده سازی موجود در این بسته در سطح پایین هستند به صورت پیشفرض می پذیریم که اجرای موازی آنها امن نخواهد بود. برخی توابع پرکاربرد این بسته عبارت اند از Copy، WriteString، ByteReader و ... که در ادامه با برخی از آنها آشنا خواهیم شد.

بسته http همانطور که از نامش پیداست توابع و عملگرهای مربوط به پروتکل HTTP در سمت کاربر و سرور را پیاده سازی کرده است. این بسته امکان Listen کردن روی پورت خاص را نیز فراهم کرده است. در پروژه تست پیش رو با این امکان بیشتر آشنا خواهیم شد.

پروژه تست

قصد داریم یک سرور ساده با امکان آپلود و دانلود فایل آماده کنیم. سرور ما دیتابیس نخواهد داشت (هدف آشنایی با io و http است). بنابراین با استفاده از ماژول ها در زبان Go پروژه جدیدی ایجاد می کنیم. می توانید در زمینه مدیریت وابستگی ها در زبان Go پست دیگر من را مطالعه کنید.

از کجا شروع کنیم؟

سرور ما قرار است دو کار ساده انجام دهد. فایل ها را با استفاده از پروتکل HTTP آپلود کند و به ما اجازه دانلود آن را بدهد. پس به دو مسیر زیر نیاز داریم.

/upload POST
/files/{fileName} GET

برای شروع این کار به چند تابع از بسته http نیاز داریم. تابع HandleFunc به ما اجازه ایجاد مسیر جدیدی را به همراه روندهای مورد نیاز خود ایجاد کنیم. تابع FileServer اجازه ایجاد سروری برای نگهداری فایل ها و تابع Dir مکان فایل ها را مشخص می کند. پس از آن باید با استفاده از تابع ListenAndServe سرور را آماده شنیدن درخواست های ورودی کنیم. امضای این توابع به شکل زیر هستند.

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func FileServer(root FileSystem) Handler
func ListenAndServe(addr string, handler Handler) error

در شاخه اصلی پروژه خود فایل fileserver.go را ایجاد کنید و قطعه کد زیر را در آن قرار دهید.

package main

import (
   &quotfmt&quot
   &quotio&quot
   &quotlog&quot
   &quotnet/http&quot
)

const (
   PORT = &quot:9393&quot
   maxUploadSize = 2 * 1024 // 2 MB
    uploadPath = &quot./tmp&quot
)

func UploadFile(w http.ResponseWriter, r *http.Request) {
   // Upload a file to an absolute path (uploadPath)
   // and return the name of the file
   if r.Method == http.MethodPost {

   } else {
      io.WriteString(w, fmt.Sprintf(&quotMethod %s is not allowed&quot, r.Method))
   }
}


func main() {
    http.HandleFunc(&quot/upload&quot, UploadFile)
    fs := http.FileServer(http.Dir(uploadPath))
    http.Handle(&quot/files/&quot, http.StripPrefix(&quot/files&quot, fs))
    log.Print(fmt.Sprintf(&quotServer started on localhost%s, use /upload for uploading files and /files/{fileName} for downloading files.&quot, PORT))
    log.Fatal(http.ListenAndServe(PORT, nil))
}

اگر پروژه را کامپایل و اجرا کنید روی پورت 9393 ماشین local آماده دریافت درخواست ها برای آدرس های زیر خواهد بود. با این روش می توانید کارکرد بسته http و توابع پر کاربرد آن را مورد بررسی قرار دهید.

http://127.0.0.1:9393/files/test-file.ext/
http://127.0.0.1:9393/upload/

فایل ها، آپلود و دانلود

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

if _, err := os.Stat(uploadPath); os.IsNotExist(err) {
   _ = os.Mkdir(uploadPath, os.ModePerm)
}

تابع UploadFile باید بتواند متدهای HTTP را تشخیص دهید و در صورتی که متد درخواست شده معتبر نبود پیغام و خطای درست را بازگرداند. برای این کار از ثابت http.MethodPost استفاده می کنیم و برای برگرداندن خطا از تابع http.Error استفاده خواهیم کرد.

در ادامه برای فایل آپلود شده را از طریق تابع FormFile دریافت کرده و نوع، اندازه و محتوای آن را بررسی می کنیم. در کد پیاده سازی شده فایل های آپلود شده باید در کلید uploadFile در درخواست ارسال شده قرار گیرند. تابع FromFile با امضای زیر فایل و هدر مربوط به آن را بازمی گرداند.

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

بررسی اندازه فایل از طریق هدر آن امکان پذیر خواهد بود. فایل بازگردانده شده از تابع FromFile از جنس io.Reader خواهد بود که می توان با استفاده از بسته ioutil تمام بایت های موجود در آن را به یکباره خواند. با استفاده از تابع ReadAll محتوای بایت های فایل آپلود شده خوانده می شود.

fileBytes, err := ioutil.ReadAll(file)
if err != nil {
   http.Error(w, &quotFile is invalid&quot, http.StatusBadRequest)
   return
} 

بررسی MimeType فایل از طریق تابع DetectContentType از بسته http امکان پذیر است. در کد پیاده سازی شده ما امکان آپلود عکس و پی دی اف را به کاربر خواهیم داد.

fileType := http.DetectContentType(fileBytes)
if fileType != &quotimage/jpeg&quot && fileType != &quotimage/jpg&quot &&
   fileType != &quotimage/gif&quot && fileType != &quotimage/png&quot &&
   fileType != &quotapplication/pdf&quot {
   http.Error(w, &quotFile type is not supported invalid&quot, http.StatusBadRequest)
   return
}

برای جلوگیری از ایجاد اشکال در زمان آپلود فایل ها با اسامی تکراری یک فایل تصادفی تولید کرده و پسوند فایل را به آن اضافه می کنیم.

fileName, err := generateToken(12)
if err != nil {
   http.Error(w, &quotFailed generating rand token&quot, http.StatusInternalServerError)
   return
}
fileEndings, err := mime.ExtensionsByType(fileType)
if err != nil {
   http.Error(w, &quotCan not read file type&quot, http.StatusInternalServerError)
   return
}

و در انتها فایل را با تابع Create بسته os ایجاد کرده و محتوای بایت های خوانده شده را در مسیر آپلود فایل ذخیره می کنیم.

newFile, err := os.Create(newPath)
if err != nil {
   http.Error(w, err.Error(), http.StatusInternalServerError)
   return
}
defer newFile.Close()
if _, err := newFile.Write(fileBytes); err != nil {
   http.Error(w, err.Error(), http.StatusInternalServerError)
   return
}

دانلود چه شد؟

برای دانلود فایل ها نیاز به پیاده سازی کد وجود ندارد و FileServer از بسته http این کار را با استفاده از نام فایل ایجاد شده انجام خواهد داد. فایل main.go در نهایت به شکل زیر در آمده است.

package main

import (
   &quotcrypto/rand&quot
   &quotencoding/hex&quot
   &quotfmt&quot
   &quotio/ioutil&quot
   &quotlog&quot
   &quotmime&quot
   &quotnet/http&quot
   &quotos&quot
   &quotpath/filepath&quot
)

const (
   PORT          = &quot:9393&quot
   maxUploadSize = 2000000 // 2 MB
   uploadPath    = &quot./upload&quot
)

func UploadFile(w http.ResponseWriter, r *http.Request) {
   // Upload a file to an absolute path (uploadPath)
   // and return the name of the file
   if r.Method == http.MethodPost {
      if err := r.ParseMultipartForm(maxUploadSize); err != nil {
         http.Error(w, fmt.Sprintf(&quotCould not parse multipart form: %v\n&quot, err), http.StatusInternalServerError)
         return
      }
      file, fileHeader, err := r.FormFile(&quotuploadFile&quot)
      if err != nil {
         http.Error(w, &quotInvalid file&quot, http.StatusBadRequest)
         return
      }
      defer file.Close()
      fileSize := fileHeader.Size
      if fileSize > maxUploadSize {
         http.Error(w, &quotFile is too large&quot, http.StatusBadRequest)
         return
      }
      fileBytes, err := ioutil.ReadAll(file)
      if err != nil {
         http.Error(w, &quotFile is invalid&quot, http.StatusBadRequest)
         return
      }
      fileType := http.DetectContentType(fileBytes)
      if fileType != &quotimage/jpeg&quot && fileType != &quotimage/jpg&quot &&
         fileType != &quotimage/gif&quot && fileType != &quotimage/png&quot &&
         fileType != &quotapplication/pdf&quot {
         http.Error(w, &quotFile type is not supported invalid&quot, http.StatusBadRequest)
         return
      }
      fileName, err := generateToken(12)
      if err != nil {
         http.Error(w, &quotFailed generating rand token&quot, http.StatusInternalServerError)
         return
      }
      fileEndings, err := mime.ExtensionsByType(fileType)
      if err != nil {
         http.Error(w, &quotCan not read file type&quot, http.StatusInternalServerError)
         return
      }
      newPath := filepath.Join(uploadPath, fileName+fileEndings[0])

      newFile, err := os.Create(newPath)
      if err != nil {
         http.Error(w, err.Error(), http.StatusInternalServerError)
         return
      }
      defer newFile.Close()
      if _, err := newFile.Write(fileBytes); err != nil {
         http.Error(w, err.Error(), http.StatusInternalServerError)
         return
      }
      w.WriteHeader(http.StatusCreated)
      _, _ = w.Write([]byte(fmt.Sprintf(&quotFileType: %s, File: %s\n&quot, fileType, newPath)))
   } else {
      http.Error(w, fmt.Sprintf(&quotMethod %s is not allowed&quot, r.Method), http.StatusMethodNotAllowed)
   }
}

// Returns a unique token based on the provided name
func generateToken(n int) (string, error) {
   bytes := make([]byte, n)
   if _, err := rand.Read(bytes); err != nil {
      return &quot&quot, err
   }
   return hex.EncodeToString(bytes), nil
}

func main() {
   if _, err := os.Stat(uploadPath); os.IsNotExist(err) {
      _ = os.Mkdir(uploadPath, os.ModePerm)
   }
   http.HandleFunc(&quot/upload&quot, UploadFile)
   fs := http.FileServer(http.Dir(uploadPath))
   http.Handle(&quot/files/&quot, http.StripPrefix(&quot/files&quot, fs))
   log.Print(fmt.Sprintf(&quotServer started on localhost%s, use /upload for uploading files and /files/{fileName} for downloading files.&quot, PORT))
   log.Fatal(http.ListenAndServe(PORT, nil))
}

در انتها

اگر می خواهید درباره توسعه REST API در زبان GO بیشتر بدانید، پست من در Level Up coding را مطالعه کنید.