freckie

Update

......@@ -12,7 +12,8 @@ type Config struct {
}
type ConfigGoogle struct {
CredentialsPath string `json:"credentials_path"`
CredentialsPath string `json:"credentials_path"`
DriveRootFolderID string `json:"drive_root_folder_id"`
}
type ConfigServer struct {
......
......@@ -12,7 +12,7 @@ import (
"github.com/julienschmidt/httprouter"
)
// GET /files/<file_id>/<sheet_id>/cell
// GET /files/<file_id>/sheets/<sheet_id>/cell
func (e *Endpoints) CellGet(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
......
......@@ -8,4 +8,5 @@ import (
type Endpoints struct {
DB *sql.DB
Sheets *utils.SheetsService
Drive *utils.DriveService
}
......
package endpoints
import (
"bytes"
"classroom/functions"
"classroom/models"
"database/sql"
"encoding/json"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/julienschmidt/httprouter"
)
......@@ -76,3 +81,237 @@ func (e *Endpoints) FilesGet(w http.ResponseWriter, r *http.Request, ps httprout
functions.ResponseOK(w, "success", resp)
}
// POST /files
func (e *Endpoints) FilesPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
if _email, ok := r.Header["X-User-Email"]; ok {
email = _email[0]
} else {
functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
return
}
// Permission Check
var isSuper int
row := e.DB.QueryRow(`
SELECT is_super FROM users WHERE email=?;
`, email)
if err := row.Scan(&isSuper); err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 401, "해당 유저가 존재하지 않습니다.")
return
}
functions.ResponseError(w, 500, "예기치 못한 에러 : "+err.Error())
return
}
if isSuper == 0 {
functions.ResponseError(w, 403, "접근 권한 부족. 관리자만 허용된 기능입니다.")
return
}
// Parse multipart/form-data
r.ParseMultipartForm(10 << 20)
// Parsing File
var fileName string
file, handler, err := r.FormFile("file")
if err != nil {
functions.ResponseError(w, 500, "파일 로드 중 예기치 못한 에러 : "+err.Error())
return
}
defer file.Close()
fileName = handler.Filename
// Create temp file
tempFile, err := ioutil.TempFile(os.TempDir(), "upload-*.tmp")
if err != nil {
functions.ResponseError(w, 500, "임시 파일 생성 중 예기치 못한 에러 : "+err.Error())
return
}
defer tempFile.Close()
// Write data to temp file
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
functions.ResponseError(w, 500, "임시 파일 작성 중 예기치 못한 에러 : "+err.Error())
return
}
tempFile.Write(fileBytes)
// [Drive] Upload file to Google Drive
reader := bytes.NewReader(fileBytes)
sheetFile, err := e.Drive.UploadFile("KHU Classroom Reservation", fileName, reader)
if err != nil {
functions.ResponseError(w, 500, "구글 드라이브에 파일 업로드 중 예기치 못한 에러 : "+err.Error())
return
}
// [Sheets] Get sheet properties of new file
props, err := e.Sheets.GetAllSheetProperties(sheetFile.Id)
if err != nil {
functions.ResponseError(w, 500, "파일 속성 불러오기 중 예기치 못한 에러 : "+err.Error())
return
}
// Querying with Transaction
tx, err := e.DB.Begin()
if err != nil {
functions.ResponseError(w, 500, "트랜잭션 시작 중 예기치 못한 에러 : "+err.Error())
return
}
defer tx.Rollback()
_, err = tx.Exec(`
INSERT INTO files (id, name)
VALUES (?, ?);
`, sheetFile.Id, fileName)
if err != nil {
functions.ResponseError(w, 500, "예기치 못한 에러 : "+err.Error())
return
}
query := "INSERT INTO sheets (id, name, file_id) VALUES "
vals := []interface{}{}
for idx := range props {
query += "(?, ?, ?),"
vals = append(vals, props[idx].SheetId, props[idx].Title, sheetFile.Id)
}
query = query[0 : len(query)-1]
stmt, _ := tx.Prepare(query)
_, err = stmt.Exec(vals...)
if err != nil {
functions.ResponseError(w, 500, "예기치 못한 에러 : "+err.Error())
return
}
err = tx.Commit()
if err != nil {
functions.ResponseError(w, 500, "예기치 못한 에러 : "+err.Error())
return
}
resp := models.FilesPostResponse{
FileID: sheetFile.Id,
}
functions.ResponseOK(w, "success", resp)
}
// POST /files/<file_id>/share
func (e *Endpoints) FilesSharePost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
if _email, ok := r.Header["X-User-Email"]; ok {
email = _email[0]
} else {
functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
return
}
// Get Path Parameters
fileID := ps.ByName("file_id")
// Permission Check
var isSuper int
row := e.DB.QueryRow(`
SELECT is_super FROM users WHERE email=?;
`, email)
if err := row.Scan(&isSuper); err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 401, "해당 유저가 존재하지 않습니다.")
return
}
functions.ResponseError(w, 500, "예기치 못한 에러 : "+err.Error())
return
}
if isSuper == 0 {
functions.ResponseError(w, 403, "접근 권한 부족. 관리자만 허용된 기능입니다.")
return
}
// Parse Request Data
type reqDataStruct struct {
UserEmails []string `json:"user_emails"`
}
var reqData reqDataStruct
if strings.Contains(r.Header.Get("Content-Type"), "application/json") {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
functions.ResponseError(w, 500, err.Error())
}
json.Unmarshal(body, &reqData)
} else {
functions.ResponseError(w, 400, "JSON 형식만 가능합니다.")
return
}
if len(reqData.UserEmails) == 0 {
functions.ResponseError(w, 400, "user_emails를 한 개 이상 보내주세요.")
return
}
// [Drive] Sharing file to users
err := e.Drive.ShareFile(fileID, reqData.UserEmails)
if err != nil {
functions.ResponseError(w, 500, "파일 권한 설정 중 예기치 못한 에러 : "+err.Error())
return
}
functions.ResponseOK(w, "success", nil)
}
// POST /files/<file_id>/protect
func (e *Endpoints) FilesProtectPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
if _email, ok := r.Header["X-User-Email"]; ok {
email = _email[0]
} else {
functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
return
}
// Get Path Parameters
fileID := ps.ByName("file_id")
// Permission Check
var isSuper int
row := e.DB.QueryRow(`
SELECT is_super FROM users WHERE email=?;
`, email)
if err := row.Scan(&isSuper); err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 401, "해당 유저가 존재하지 않습니다.")
return
}
functions.ResponseError(w, 500, "예기치 못한 에러 : "+err.Error())
return
}
if isSuper == 0 {
functions.ResponseError(w, 403, "접근 권한 부족. 관리자만 허용된 기능입니다.")
return
}
// [Sheets] Get sheet properties of new file
props, err := e.Sheets.GetAllSheetProperties(fileID)
if err != nil {
functions.ResponseError(w, 500, "파일 속성 불러오기 중 예기치 못한 에러 : "+err.Error())
return
}
var sheetIDs []int64
for idx := range props {
sheetIDs = append(sheetIDs, props[idx].SheetId)
}
// [Sheets] Protect all sheets
err = e.Sheets.ProtectAll(fileID, sheetIDs)
if err != nil {
functions.ResponseError(w, 500, "셀 보호 설정 중 예기치 못한 에러 : "+err.Error())
return
}
functions.ResponseOK(w, "success", nil)
}
......
......@@ -15,7 +15,7 @@ import (
"github.com/julienschmidt/httprouter"
)
// POST /files/<file_id>/<sheet_id>/reservation
// POST /files/<file_id>/sheets/<sheet_id>/reservation
func (e *Endpoints) ReservationPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
......@@ -190,7 +190,7 @@ loopCheckingValidation:
functions.ResponseOK(w, "success", resp)
}
// DELETE /files/<file_id>/<sheet_id>/reservation/<reservation_id>
// DELETE /files/<file_id>/sheets/<sheet_id>/reservation/<reservation_id>
func (e *Endpoints) ReservationDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
......
......@@ -6,4 +6,6 @@ require (
classroom/models v0.0.0
)
replace classroom/models v0.0.0 => ../models
replace (
classroom/models v0.0.0 => ../models
)
......
......@@ -59,7 +59,11 @@ func main() {
defer db.Close()
// Google API Setting
sheets, err := utils.NewSheetsService(cfg.Google.CredentialsPath)
drive, err := utils.NewDriveService(cfg.Google.CredentialsPath, cfg.Google.DriveRootFolderID) // save token with drive, sheets scope
if err != nil {
log.Fatal(err)
}
sheets, err := utils.NewSheetsService(cfg.Google.CredentialsPath) // just load token
if err != nil {
log.Fatal(err)
}
......@@ -67,6 +71,7 @@ func main() {
ep := endpoints.Endpoints{
DB: db,
Sheets: sheets,
Drive: drive,
}
// Router Setting
......@@ -75,9 +80,12 @@ func main() {
router.GET("/api/users", ep.UsersGet)
router.POST("/api/users", ep.UsersPost)
router.GET("/api/files", ep.FilesGet)
router.GET("/api/files/:file_id/:sheet_id/cell", ep.CellGet)
router.POST("/api/files/:file_id/:sheet_id/reservation", ep.ReservationPost)
router.DELETE("/api/files/:file_id/:sheet_id/reservation/:reservation_id", ep.ReservationDelete)
router.POST("/api/files", ep.FilesPost)
router.POST("/api/files/:file_id/share", ep.FilesSharePost)
router.POST("/api/files/:file_id/protect", ep.FilesProtectPost)
router.GET("/api/files/:file_id/sheets/:sheet_id/cell", ep.CellGet)
router.POST("/api/files/:file_id/sheets/:sheet_id/reservation", ep.ReservationPost)
router.DELETE("/api/files/:file_id/sheets/:sheet_id/reservation/:reservation_id", ep.ReservationDelete)
// Local Mode
portStr := strconv.Itoa(cfg.Server.Port)
......
......@@ -10,3 +10,7 @@ type FilesGetItem struct {
FileName string `json:"file_name"`
CreatedAt string `json:"created_at"`
}
type FilesPostResponse struct {
FileID string `json:"file_id"`
}
......
package utils
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"golang.org/x/oauth2/google"
"google.golang.org/api/drive/v3"
"google.golang.org/api/option"
)
// DriveService is a wrapper for Google Drive Service and its context.
type DriveService struct {
srv *drive.Service
ctx context.Context
driveRootFolderID string
}
// NewDriveService is a factory function which returns a new DriveService{}.
func NewDriveService(credentialsPath, driveRootFolderID string) (*DriveService, error) {
b, err := ioutil.ReadFile(credentialsPath)
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
// If modifying these scopes, delete your previously saved token.json.
config, err := google.ConfigFromJSON(b,
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/spreadsheets",
)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
client := getClient(config)
ctx := context.Background()
driveService, err := drive.NewService(ctx,
option.WithHTTPClient(client),
option.WithScopes(drive.DriveScope),
)
if err != nil {
return nil, err
}
srv := &DriveService{
srv: driveService,
ctx: ctx,
driveRootFolderID: driveRootFolderID,
}
return srv, nil
}
// UploadFile makes a request that uploads file to specific folder.
func (s *DriveService) UploadFile(folderName, fileName string, content io.Reader) (*drive.File, error) {
file, err := s.createFile(fileName, s.driveRootFolderID, content)
if err != nil {
return nil, err
}
return file, nil
}
// ShareFile makes a request that shares the file for specific users.
func (s *DriveService) ShareFile(fileID string, emails []string) error {
for _, email := range emails {
perm := &drive.Permission{
Type: "user",
Role: "writer",
EmailAddress: email,
}
resp, err := s.srv.Permissions.Create(fileID, perm).Do()
if err != nil {
fmt.Println(resp, err)
continue
}
}
return nil
}
// createFolder creates a folder and returns its object.
func (s *DriveService) createFolder(name, parentID string) (*drive.File, error) {
d := &drive.File{
Name: name,
MimeType: "application/vnd.google-apps.folder",
Parents: []string{parentID},
}
file, err := s.srv.Files.Create(d).Do()
if err != nil {
return nil, err
}
return file, nil
}
// createFile creates a file and returns its object.
func (s *DriveService) createFile(name, parentID string, content io.Reader) (*drive.File, error) {
f := &drive.File{
Name: name,
MimeType: "application/vnd.google-apps.spreadsheet",
Parents: []string{parentID},
}
file, err := s.srv.Files.Create(f).Media(content).Do()
if err != nil {
return nil, err
}
return file, nil
}
type UploadRequest struct {
FolderName string
FileName string
MimeType string
Content io.Reader
}
module classroom
module classroom/utils
go 1.14
......
......@@ -11,11 +11,13 @@ import (
"google.golang.org/api/sheets/v4"
)
// SheetsService is a wrapper for Spread Sheets Service and its context.
type SheetsService struct {
srv *sheets.Service
ctx context.Context
}
// NewSheetsService is a factory function which returns a new SheetsService{}.
func NewSheetsService(credentialsPath string) (*SheetsService, error) {
b, err := ioutil.ReadFile(credentialsPath)
if err != nil {
......@@ -46,6 +48,7 @@ func NewSheetsService(credentialsPath string) (*SheetsService, error) {
return srv, nil
}
// WriteAndMerge makes requests that merge cells and write value into cells.
func (s *SheetsService) WriteAndMerge(sr SheetsRequest) error {
req := &sheets.Request{}
req.MergeCells = &sheets.MergeCellsRequest{
......@@ -73,6 +76,7 @@ func (s *SheetsService) WriteAndMerge(sr SheetsRequest) error {
return nil
}
// RemoveValue makes requests that clear and unmerge cells.
func (s *SheetsService) RemoveValue(sr SheetsRequest) error {
req := &sheets.Request{}
req.UnmergeCells = &sheets.UnmergeCellsRequest{
......@@ -109,6 +113,54 @@ func (s *SheetsService) RemoveValue(sr SheetsRequest) error {
return nil
}
func (s *SheetsService) GetAllSheetProperties(fileID string) ([]*sheets.SheetProperties, error) {
var result []*sheets.SheetProperties
req, err := s.srv.Spreadsheets.Get(fileID).Do()
if err != nil {
return nil, err
}
for idx := range req.Sheets {
result = append(result, req.Sheets[idx].Properties)
}
return result, nil
}
func (s *SheetsService) ProtectAll(fileID string, sheetIDs []int64) error {
reqs := []*sheets.Request{}
for _, sheetID := range sheetIDs {
req := &sheets.Request{}
req.AddProtectedRange = &sheets.AddProtectedRangeRequest{
ProtectedRange: &sheets.ProtectedRange{
Range: &sheets.GridRange{
SheetId: sheetID,
StartColumnIndex: 0,
EndColumnIndex: 100,
StartRowIndex: 0,
EndRowIndex: 1000,
},
},
}
reqs = append(reqs, req)
}
rb := &sheets.BatchUpdateSpreadsheetRequest{
Requests: reqs,
}
_, err := s.srv.Spreadsheets.BatchUpdate(fileID, rb).Context(s.ctx).Do()
if err != nil {
return err
}
return nil
}
// SheetsRequest is a wrapper for request, especially WriteAndMerge() and RemoveValue() functions.
type SheetsRequest struct {
SpreadSheetID string
SheetName string
......@@ -118,6 +170,7 @@ type SheetsRequest struct {
Value string
}
// NewSheetsRequest is a factory function which returns a new SheetsRequest{}.
func NewSheetsRequest(
spreadSheetID, sheetName string, sheetID int64, column string, start, end int64, value string) SheetsRequest {
colIndex := A1ToInt(column)
......