Category: [บันทึกกันลืม]

  • [บันทึกกันลืม] วิธีย้ายทั้ง Folder จาก Google Drive ไปยัง Shared Drives ขององค์กร

    จากที่ Google Apps for Education หรือ Google Workspace for Education ปัจจุบัน ซึ่งจากเดิม ชูนโยบาย Unlimited Storage มาเป็น 100 GB ต่อ คน และ 100 TB ต่อ องค์กร — เพียงพอ ต่อการใช้งานเพื่อการศึกษาจริง ๆ ถ้าจัดการให้ดี

    เหตุแห่งการเปลี่ยนแปลง

    ผมว่า ก็เรา ๆ นี่แหล่ะ ใช้พื้นที่เค้าแบบไม่ใช่เพื่อการศึกษา ต้องยอมรับก่อน แล้วเรามาแก้ไขกัน

    • มีบางคนในองค์กร เอาไปเก็บไฟล์หนัง ไม่เกี่ยวกับการศึกษา จริงหรือไม่ รู้อยู่แก่ใจ, เรื่องนี้ Super Admin ขององค์กรตรวจสอบได้ ด้วย Google Vault จัดการให้เด็ดขาด (Super Admin รายงานได้ ผู้มีอำนาจต้องตัดสินใจ … กล้าไม๊ ค่อยมาดูกัน)
    • บางองค์กรไม่มีมาตรการควบคุมบัญชี สร้างมากเกินความจำเป็น แล้วบางคนเอาบัญชีไปขายใน Lazada, Shopee จริงไม่จริง รู้อยู่แก่ใจ
    • บางคน เอาแต่ประโยชน์ส่วนตน ได้สิทธิ์ แต่เอาพื้นที่ Shared Drives ไปปล่อยให้ เว็บโป๊ เว็บหนังซีรี่ ใช้พื้นที่ซะงั้น

    ก็เป็นธรรมดา Google ลงทุนทรัพยากร แต่ไม่ได้ใช้ให้ถูกตามวัตถุประสงค์ ก็ต้องมีการควบคุม งานส่วนตัว ใช้ My Drive, งานของหน่วยงาน ใช้ Shared Drives จบ

    (บางคนบอกว่า ย้ายไป Microsoft OneDrive ก็ได้วะ 5TB … ทำเหมือนเดิม เดี๋ยวก็จะได้ผลเหมือนเดิม)

    ผมได้เล่าไว้บ้างแล้ว เกี่ยวกับการใช้พื้นที่ของ Shared Drives

    มาดูวิธีแก้ปัญหากันดีกว่า

    • จากเดิม คนในสำนักงานก็จะชอบใช้ My Drive แล้ว Share ให้คนในหน่วยงานใช้กัน ก็เข้าใจได้ เพราะตอนแรกไม่มี Shared Drives เคยทำอย่างไรก็จะทำอย่างนั้น ไม่ชอบเปลี่ยนแปลง ถึงเวลาต้องเปลี่ยนแล้ว คือ งานส่วนรวม เอาไปเก็บใน Shared Drive
    • เอาหล่ะ สมมุติว่า เราเคยใช้ Folder หนึ่ง เก็บสารพัดสิ่งที่เกี่ยวกับงานจริง ๆ แต่มันอยู่ใน My Drive (พื้นที่ส่วนตัว) สมมุติใช้ Folder ชื่อ parent ในนั้นมี Sub folder ย่อย และมีไฟล์ย่อย ๆ ซ้อน ๆ ลงไปจำนวนมาก
    parent folder ซึ่งเป็นงานจริง ๆ และมีไฟล์ และ folder ย่อย ๆ ลงไปจำนวนมาก
    • แนะนำว่า แต่ละหน่วยงาน เช่น ระดับภาควิชา หรือ ระดับหน่วยงาน ไปสร้างพื้นที่ไว้บน Shared Drives

    ปัญหาคือ … การย้ายไฟล์จาก​ Google Drives ไปยัง Shared Drives นั้น ทำได้ในระดับไฟล์ ไม่สามารถย้ายทั้ง Folder ได้

    ใครใคร่ย้าย ก็เลือกไฟล์ แล้วคลิกขวา Move to ไป

    แต่กรณี ของผมมีไฟล์ ที่สร้างจากอุปกรณ์ ขนาด 400-800 kb จำนวน …. 60 GB คิดคร่าว ๆ 2 ไฟล์ เป็น 1 MB ก็ราว ๆ 60 x 1024 x 2 = 122,880 ไฟล์

    กรณีย้ายไฟล์จำนวนมากใน Folder เดียวกัน ไปไว้บน​ Shared Drives

    ผมสร้าง Google Apps Script ไว้ที่ https://gist.github.com/nagarindkx/70f254a21c5cad7f964e762ab8b2e733

    function moveFilesFromDriveToShareDrive() {
      /*
      Created by: Kanakorn Horsiritham
      Digital Innovation and Data Analytics (DIDA)
      Faculty of Medicine
      Prince of Songkla University
      Hatyai, Songkhla
      THAILAND
      */
      sourceFS=DriveApp.getFolderById('GoogleDrive-Folder-ID').getFiles()
      destinationFS=DriveApp.getFolderById('SharedDrives-Folder-ID')
      
      while(sourceFS.hasNext()){
        f=sourceFS.next()
        f.moveTo(destinationFS) 
      }
    }

    สิ่งที่ท่านต้องทำคือ

    1. เปิด Google Drive ต้นทางบน Web Browser แล้วดูที่ URL แล้ว Copy เฉพาะ GoogleDrive-Folder-ID เช่น

      https://drive.google.com/drive/folders/GoogleDrive-Folder-ID

      เก็บไว้ก่อน

    2. เปิด Shared Drives ปลายทาง แล้วทำเหมือนกัน

    3. จากนั้น สร้าง Google Apps Script ใน​ Google Drives

    4. ตั้งชื่อ Project ว่า movefile1

    5. copy code ข้างต้นมาใส่ จากนั้นแก้ไข GoogleDrive-Folder-ID และ SharedDrives-Folder-ID จากข้อ 1,2 แล้วกดปุ่ม Save

    6. ถ้าไฟล์ไม่มากนัก ก็ เลือก moveFilesFromDriveToShareDrive กดปุ่ม Run

    ถ้าทำครั้งแรก ระบบอาจจะถามอย่างนี้ ก็ให้เลือก Review Permissions แล้ว Next Next เลือก Account ไป (ขั้นนี้แล้ว ไม่อธิบายรายละเอียดแล้วนะครับ)

    จากนั้นก็รอไป Script จะทำงานต่อเนื่อง ยาวนานไม่เกิน 1800 วินาที หรือ 30 นาที ผลที่ได้คือ เฉพาะ Files เท่านั้น ที่จะย้ายไปอยู่ใน Shared Drives แต่ใจเย็น ๆ มีวิธีพาไปทั้งยวง รอแป๊บ

    7. แต่ถ้ามีไฟล์จำนวนมาก จนครบ 30 นาทีแล้ว ก็ยังไม่หมด ก็ต้องอาศัย Trigger เพื่อสั่งให้ทำทุก ๆ 1 ชั่วโมง ดังนี้
    เลื่อน Cursor ไปทางซ้ายมือ คลิกที่ Triggers

    คลิกปุ่ม Add Trigger

    เลือกตามนี้ แล้ว Save

    จากนั้นก็รอไป ระบบจะทำจนหมดเวลา 30 นาที รออีก 1 ชั่วโมง แล้วก็ทำต่อไปเรื่อย ๆ

    อันนี้ ของผม

    ยังอีกยาวนาน

    กรณีย้ายทั้ง Folder ไปไว้บน​ Shared Drives

    UPDATE!!!!

    เลือก Folder ต้นทาง และ ปลายทาง จากนั้น copy/move ได้เลย โดย ที่จะสร้างโครงสร้าง Folders และ Files ที่ Shared Drives ตามเดิมจากต้นทาง

    ผมทำ Script ไว้ที่ https://gist.github.com/nagarindkx/e2897455b2ede2ae168f8cd5ccf16982

    function copyFilesFromDriveToShareDrive() {
      /*
      Created by: Kanakorn Horsiritham
      Digital Innovation and Data Analytics (DIDA)
      Faculty of Medicine
      Prince of Songkla University
      Hatyai, Songkhla
      THAILAND
      */
      
      sourceID='GoogleDrive-Folder-ID'
      destinationID='SharedDrives-Folder-ID' 
       
      copyFolder(sourceID, destinationID)
      //moveFolder(sourceID, destinationID)
    
    }
    
    function copyFolder(sourceID, destinationID){
      sFD = DriveApp.getFolderById(sourceID)
      dFD = DriveApp.getFolderById(destinationID)
      
      files = sFD.getFiles()
      while(files.hasNext()) {
        f=files.next()
        if (! dFD.getFilesByName(f.getName()).hasNext()) {
          f.makeCopy(f.getName(), dFD)
          console.log('copy file: ' + f.getName())
        } else {
          Logger.log( "file: " + f.getName() + ' exist')
        }
        
      }
    
      folders=sFD.getFolders()
      nextlevel=[]
      while(folders.hasNext()) {
        fd = folders.next()
        tmpDestinationFolder = dFD.getFoldersByName(fd.getName())
        if (! tmpDestinationFolder.hasNext()) {
          new_folder = dFD.createFolder(fd.getName())
          Logger.log( "create folder: " + fd.getName())
        } else {
          new_folder = tmpDestinationFolder.next()
          Logger.log( "folder: " + fd.getName() + ' exist')
        }
        
        nextlevel.push([fd.getId(), new_folder.getId()])
      }
      nextlevel.forEach(function(r){
        copyFolder(r[0], r[1])
      })
    }
    
    
    function moveFolder(sourceID, destinationID){
      sFD = DriveApp.getFolderById(sourceID)
      dFD = DriveApp.getFolderById(destinationID)
      
      files = sFD.getFiles()
      while(files.hasNext()) {
        f=files.next()
        if (! dFD.getFilesByName(f.getName()).hasNext()) {
          console.log('file: ' + f.getName())
          f.moveTo( dFD)
          console.log('move file: ' + f.getName())
        } else {
          Logger.log( "file: " + f.getName() + ' exist')
        }
      }
    
      folders=sFD.getFolders()
      nextlevel=[]
      c=0
    
      if (folders.hasNext()) {
        while(folders.hasNext()) {
          fd = folders.next()
          tmpDestinationFolder = dFD.getFoldersByName(fd.getName())
          if (! tmpDestinationFolder.hasNext()) {
            new_folder = dFD.createFolder(fd.getName())
          } else {
            new_folder = tmpDestinationFolder.next()
            Logger.log( "folder: " + fd.getName() + ' exist')
          }
          nextlevel.push([fd.getId(), new_folder.getId()])
          c=c+1
        }
        nextlevel.forEach(function(r){
          moveFolder(r[0], r[1])
        })
      } else {
        sFD.setTrashed(true)
      }
      
    }
  • [บันทึกกันลืม] แก้ปัญหารัน Shell Script กับมือทุกอย่างสมบูรณ์ แต่พอใช้ cron แล้วพัง

    เรา “จำกันมา” ว่า การเขียน Shell Script นั้น ทำด้วยมืออย่างไร ก็เขียนลงไปในไฟล์ แล้วเอา Shell เช่น Bash ไป Run แล้วเอาไปตั้งในไฟล์ /etc/cron.d/mycron

    ปัญหาคือ

    บางทีเขียนด้วยภาษา Script เช่น python, R แล้วบางที ก็อยากจะเพิ่มภาษาไทยเข้าไป

    ตามสูตร ก็ต้องใช้คำสั่ง locale ตรวจสอบ

    LANG=en_US.UTF-8
    LANGUAGE=en_US.UTF-8
    LC_CTYPE="en_US.UTF-8"
    LC_NUMERIC="en_US.UTF-8"
    LC_TIME="en_US.UTF-8"
    LC_COLLATE="en_US.UTF-8"
    LC_MONETARY="en_US.UTF-8"
    LC_MESSAGES="en_US.UTF-8"
    LC_PAPER="en_US.UTF-8"
    LC_NAME="en_US.UTF-8"
    LC_ADDRESS="en_US.UTF-8"
    LC_TELEPHONE="en_US.UTF-8"
    LC_MEASUREMENT="en_US.UTF-8"
    LC_IDENTIFICATION="en_US.UTF-8"
    LC_ALL=en_US.UTF-8

    ก็จะพบว่า เป็น UTF-8 สวยงาม

    เขียนใส่ข้อมูลที่ประมวลผลมา ก็จะได้เป็นภาษาไทยสวยงาม

    สวัสดีชาวโลก

    แต่ พอเอาไปใส่ Shell Script ผลออกมาเป็นตัวอักษรบนตัวเลข ซึ่งนั่นคือ UTF-8 HEX code

    <U+0E2B><U+0E32><U+0E14><U+0E43><U+0E2B><U+0E0D><U+0E48>

    เหตุของปัญหา

    ก็ลองเอา คำสั่ง locale ไปใส่ใน Shell Script แล้วดูผลในไฟล์พบว่า

    LANG=
    LANGUAGE=
    LC_CTYPE="POSIX"
    LC_NUMERIC="POSIX"
    LC_TIME="POSIX"
    LC_COLLATE="POSIX"
    LC_MONETARY="POSIX"
    LC_MESSAGES="POSIX"
    LC_PAPER="POSIX"
    LC_NAME="POSIX"
    LC_ADDRESS="POSIX"
    LC_TELEPHONE="POSIX"
    LC_MEASUREMENT="POSIX"
    LC_IDENTIFICATION="POSIX"
    LC_ALL=

    สรุปคือ locale ตอนใช้ cron เป็น Default อย่างนี้ (พอดีทำงานบน docker ที่ base image มาจาก debian)

    วิธีแก้ไขปัญหา

    google สิครับ รออะไร ไปพบข้อมูลจาก https://www.logikdev.com/2010/02/02/locale-settings-for-your-cron-job/

    คือ ให้ใส่

    SHELL=/bin/bash
    LANG=en_US.UTF-8
    LANGUAGE=en
    LC_CTYPE=en_US.UTF-8

    ลงไปใน cron ด้วย อย่างเช่น

    SHELL=/bin/bash
    LANG=en_US.UTF-8
    LANGUAGE=en
    LC_CTYPE=en_US.UTF-8
    */3 * * * * /bin/bash /code/somescript.sh

    แล้วก็อย่าลืม

    /etc/init.d/cron restart

    เป็นอันเรียบร้อย

    หวังว่าจะมีประโยชน์ครับ

  • [บันทึกกันลืม] วิธีซ่อน choice ที่ไม่จำเป็น ด้วย jQuery

    ปัญหามีอยู่ว่า ในการเขียน Web Application ด้วย django แบบฟอร์มที่ซับซ้อนหน่อย จะมีประมาณว่า ถ้าเลือกข้อนี้ ให้ขึ้นคำถามอีกข้อขึ้นมาถาม

    เช่น ถ้าเลือกข้อ “รพ.สงขลานครินทร์” ให้ขึ้นคำถาม “ลักษณะการทำงาน” ซึ่งก็ไม่ได้ยากอะไร

    แต่ว่า คำตอบของข้อนี้ ในกรณีนี้ จะต้องบังคับตอบ แต่ ถ้าเลือก “สถานพยาบาลอื่น ๆ” จะต้องไม่บังคับตอบ

    ปัญหาคือ เจ้า Choice ที่ เมื่อไม่จำเป็นต้องตอบ มันต้องไม่มีค่า แต่ตอนที่มันต้องใช้ตอบ (อย่างในภาพ) มันควรจะหายไป แต่มันไม่หายไป (เข้าใจยากหน่อยนะ)

    ลองใช้

    $('#id_q2_1_work_3').hide();

    ก็ไม่หาย

    วิธีแก้คือ ไป hide label

    $("label[for='id_q2_1_work_3']").hide()

    เวิร์คเลย

    จบ

  • [บันทึกกันลืม] แก้ปัญหา Plotly ไม่แสดง Chart บน JuputerLab

    ในการ Visualization บน Jupyter Notebook, Juputer Lab พื้นฐานก็จะใช้ matplotlib, seaborn และเพื่อให้ทำ Interactive ได้มากขึ้น ก็จะไปใช้ Plotly และ Dash

    ปัญหาคือ ถ้าเริ่มต้นใช้งาน Juputer Notebook / Jupyter Lab แล้วติดตั้ง plotly, dash package แล้ว บน Jupyter ก็ไม่แสดงผลแบบ Inline แบบนี้

    ค้นหาพักใหญ่ ก็พบว่า อ้อ ต้องติดตั้ง jupyter labextension ด้วย วิธีตรวจสอบว่า ตอนนี้มี Extension อะไรอยู่บ้าง ใช้คำสั่งต่อไปนี้

    jupyter labextension list

    ผลคือ โดยค่าเริ่มต้น ไม่มี jupyterlab-plotly กับ plotlywidget ติดตั้งอยู่

    วิธีการติดตั้ง ใช้คำสั่งต่อไปนี้

    ! jupyter labextension install jupyterlab-plotly plotlywidget
    ! jupyter lab build

    แล้ว restart kernel

    จากนั้น ลอง jupyter labextension list อีกครั้ง

    แล้วก็จะสามารถใช้ Plotly ได้

  • วิธี Bypass หน้า Privacy Error เนื่องจาก NET::ERR_CERT_INVALID บน Google Chrome

    Google Chrome พยายามปกป้องเราจาก Website ที่อาจจะมีความเสี่ยง ใช่ ดี แต่ว่า บางทีก็จำเป็น แบบว่า เป็นเครื่องทดสอบไรงี้ ยังไม่ได้ติดตั้ง Certificate จริงจัง

    ถ้าเจอหน้านี้ … คลิก Advanced บางทีก็จะเจอปุ่ม Proceed to website ก็จะเข้าได้

    แต่ถ้าไม่มีปุ่มนั้นหล่ะ ?!?!

    วิธีการง่ายสุด คือ พิมพ์คำว่า

    thisisunsafe

    พิมพ์ไปเลย ไม่ต้องเคาะ Enter ก็จะเข้าเว็บไซต์ได้เลย

    *** ใช้ในกรณีเป็น เว็บไซต์ทดสอบของตัวเองเท่านั้นนะ ***

  • [บันทึกกันลืม] วิธีติดตั้ง django-auth-ldap

    การจะใช้ ldap backend บน django นั้น จะต้องติดตั้ง django-auth-ldap ซึ่งจะใช้คำสั่ง

    pip install django-auth-ldap

    แต่ว่า อาจจะเจอ Error ประมาณนี้

    เหตุเพราะ ใน container ที่ใช้ อาจจะไม่มีเครื่องมือในการ build

    วิธีแก้ไขคือ (ใน docker python:3.8) ให้ติดตั้ง package

    apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev python-ldap build-essential

    แล้วจึงค่อยติดตั้งผ่าน pip

    pip install django-auth-ldap

    ก็จะเรียบร้อยครับ

  • วิธีสร้าง reverse proxy ด้วย Apache2 docker ด้วย subpath ใน Domain Name เดียวกัน

    ในการเขียน web application บน docker (ผมชอบใช้ django / flask) เมื่อต้องการ deploy ไว้บนเครื่อง Production Server ซึ่งแต่ละ application ก็จะเปิด port ต่าง ๆ กัน เช่น 8080, 8081, 8082 … ว่ากันไป

    เช่น จะมี URL เป็น IP Address แล้วตามด้วย Port อย่างนี้

    192.168.x.y:8080
    192.168.x.y:8081
    192.168.x.y:8082

    การสร้าง Reverse Proxy ทำให้ผู้ใช้สามารถ เข้าถึง web application เหล่านี้ได้ง่ายขึ้น โดยใช้วิธีการ เช่น ใช้ Web Server อย่าง Apache, Nginx ทำงานร่วมกับ DNS Server เพื่อจด Domain Name มายัง IP Address เช่น

    app1 IN A 192.168.x.y
    app2 IN A 192.168.x.y
    app3 IN A 192.168.x.y

    แล้วที่ Web Server ค่อยมากำหนดว่า เมื่อ ServerName เป็น app1.xxx.psu.ac.th จะส่งไปยัง 192.168.x.y:8080 อะไรทำนองนี้

    ปัญหาคือ ถ้าต้องจดทะเบียน DNS ไปเรื่อย ๆ ก็คงไม่ไหว อีกทั้ง เมื่อต้องใช้ HTTPS ก็ต้องไปทำทีละ domain name อีกด้วย

    แล้วจะทำอย่างไร ให้สามารถรวบ web application ที่อยู่บน production server ตัวเดียวกันนี้ ให้อยู่ใน domain name เดียวกัน แต่ แยกกันด้วย URL Path ด้านหลังแทน เช่น

    xxx.psu.ac.th/app1/
    xxx.psu.ac.th/app2/
    xxx.psu.ac.th/app3/

    และ ในเมื่อเราใช้ Docker บน Production Server อยู่แล้ว ก็ใช้ Web Server เป็น container ซะ

    วิธีการทำมีดังนี้

    1. สร้าง apache2 container ให้ชื่อว่า httpd รับการเชื่อมต่อที่ port 80 ด้วยคำสั่ง
    docker run -dit --name httpd -p 80:80 httpd:2.4

    2. ตอนนี้ เราจะได้ container ชื่อ httpd มาแล้ว ต่อไป copy ไฟล์ httpd.conf ออกมา ด้วยคำสั่ง

    docker cp httpd:/usr/local/apache2/conf/httpd.conf .

    3. ตอนนี้ เราจะได้ไฟล์ httpd.conf ออกมา ให้แก้ไขไฟล์ด้วย Text Editor ตามถนัด โดยการทำ Reverse Proxy ด้วย apache2 นั้น ต้องเปิดใช้ module ดังนี้ (ค้นหา และ uncomment) แล้ว Save

    LoadModule proxy_html_module modules/mod_proxy_html.so
    LoadModule proxy_module modules/mod_proxy.so
    LoadModule proxy_http_module modules/mod_proxy_http.so
    LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so

    4. เพิ่ม Directive “ProxyPass” และ “ProxyPassReverse” ตามด้วย URL Path ที่ต้องการของแต่ละ Application แล้ว URL ที่สร้างไว้ เสร็จแล้ว ก็ Save

    ProxyPass "/app1/"  "http://192.168.x.y:8080/"
    ProxyPassReverse "/app1/"  "http://192.168.x.y:8080/"
    ProxyPass "/app2/"  "http://192.168.x.y:8081/"
    ProxyPassReverse "/app2/"  "http://192.168.x.y:8081/"
    ProxyPass "/app3/"  "http://192.168.x.y:8082/"                
    ProxyPassReverse "/app3/"  "http://192.168.x.y:8082/"          

    5. ต่อไป ก็ copy file httpd.conf ที่แก้ไขแล้ว กลับเข้าไปใน container ด้วยคำสั่ง

    docker cp httpd.conf httpd:/usr/local/apache2/conf/httpd.conf

    6. สุดท้าย restart container ด้วยคำสั่ง

    docker restart httpd

    ก็ประมาณนี้ เวลาจะเพิ่ม หรือ เปลี่ยนอะไร ก็ ทำซ้ำขั้นตอน 4,5,6 ครับ

    หมายเหตุ: ในบาง web application อย่างเช่น Grafana ก็จะต้องไปแก้ไข root_url ให้ใช้ subpath ด้วย ในพวก django, flask ต้อง ALLOWED_HOST อีกเล็กน้อย ครับผม