test upload file to server

This commit is contained in:
Kenta420 2024-02-05 11:45:54 +07:00
parent 16e0e4f9d8
commit aaa60216b2
43 changed files with 1814 additions and 285 deletions

View file

@ -1,9 +1,9 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
"es2021": true, es2021: true,
"node": true, node: true,
"browser": false browser: false
}, },
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
@ -11,23 +11,16 @@ module.exports = {
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
'prettier' 'prettier'
], ],
ignorePatterns: [ ignorePatterns: ['types/env.d.ts', 'node_modules/**', '**/dist/**', 'scr/components/ui/**'],
"types/env.d.ts",
"node_modules/**",
"**/dist/**",
],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
ecmaVersion: 12, ecmaVersion: 12,
sourceType: 'module', sourceType: 'module'
}, },
plugins: ['react-refresh', '@typescript-eslint', '@tanstack/eslint-plugin-query'], plugins: ['react-refresh', '@typescript-eslint', '@tanstack/eslint-plugin-query'],
rules: { rules: {
'react-refresh/only-export-components': [ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'warn', '@typescript-eslint/consistent-type-imports': 'error',
{ allowConstantExport: true }, 'react-hooks/exhaustive-deps': 'off'
], }
"@typescript-eslint/consistent-type-imports": "error",
"react-hooks/exhaustive-deps": "off",
},
} }

View file

@ -49,3 +49,7 @@ In this section, we will list all the environment variables. That app will use i
### TAOBIN_RECIPE_MANAGER_SERVER_URL ### TAOBIN_RECIPE_MANAGER_SERVER_URL
- The url of the server. Default: `http://localhost:8080` - The url of the server. Default: `http://localhost:8080`
### TAOBIN_RECIPE_MANAGER_TUS_SERVER_URL
- The url of the tus server for upload/download files. Default: `http://localhost:8090/files`

View file

@ -26,7 +26,7 @@ export default function (
picture: params.get('picture'), picture: params.get('picture'),
permissions: params.get('permissions'), permissions: params.get('permissions'),
access_token: params.get('access_token'), access_token: params.get('access_token'),
max_age: params.get('max_age'), max_age: params.get('access_token_max_age'),
refresh_token: params.get('refresh_token') refresh_token: params.get('refresh_token')
}) })
} }

View file

@ -1,4 +1,4 @@
import { findCredentials, getPassword, setPassword } from '@postman/node-keytar' import { findCredentials, getPassword, setPassword, deletePassword } from '@postman/node-keytar'
export function eventGetKeyChain(icpMain: Electron.IpcMain) { export function eventGetKeyChain(icpMain: Electron.IpcMain) {
icpMain.on('get-keyChain', (event, serviceName, account) => { icpMain.on('get-keyChain', (event, serviceName, account) => {
@ -7,12 +7,12 @@ export function eventGetKeyChain(icpMain: Electron.IpcMain) {
}) })
}) })
icpMain.on('set-keyChain', (_event, serviceName, account, password) => { icpMain.on('set-keyChain', (_event, { serviceName, account, password }) => {
setPassword(serviceName, account, password) setPassword(serviceName, account, password)
}) })
icpMain.on('delete-keyChain', (_event, serviceName, account) => { icpMain.on('delete-keyChain', (_event, { serviceName, account }) => {
setPassword(serviceName, account, '') deletePassword(serviceName, account)
}) })
icpMain.handle('keyChainSync', async (_event, serviceName) => { icpMain.handle('keyChainSync', async (_event, serviceName) => {

8
client-electron/index.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
import 'react'
declare module 'react' {
// add webkitdirectory to input element React.InputHTMLAttributes<HTMLInputElement>
interface HTMLInputElement {
webkitdirectory?: boolean
}
}

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,8 @@
"prebuild:electron": "rm -rf dist-renderer && rm -rf dist-electron && vite optimize -c vite.config.electron.ts -m production", "prebuild:electron": "rm -rf dist-renderer && rm -rf dist-electron && vite optimize -c vite.config.electron.ts -m production",
"prebuild:web": "rm -rf dist-web && vite optimize -c vite.config.web.ts -m production", "prebuild:web": "rm -rf dist-web && vite optimize -c vite.config.web.ts -m production",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview:web": "vite preview -c vite.config.web.ts" "preview:web": "vite preview -c vite.config.web.ts",
"postinstall": "electron-builder install-app-deps"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
@ -35,6 +36,14 @@
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-query": "^5.17.19", "@tanstack/react-query": "^5.17.19",
"@tanstack/react-table": "^8.11.7", "@tanstack/react-table": "^8.11.7",
"@uppy/core": "^3.8.0",
"@uppy/dashboard": "^3.7.1",
"@uppy/drag-drop": "^3.0.3",
"@uppy/file-input": "^3.0.4",
"@uppy/golden-retriever": "^3.1.1",
"@uppy/progress-bar": "^3.0.4",
"@uppy/react": "^3.2.1",
"@uppy/tus": "^3.5.0",
"@yume-chan/adb": "^0.0.22", "@yume-chan/adb": "^0.0.22",
"@yume-chan/adb-credential-web": "^0.0.22", "@yume-chan/adb-credential-web": "^0.0.22",
"@yume-chan/adb-daemon-webusb": "^0.0.22", "@yume-chan/adb-daemon-webusb": "^0.0.22",
@ -49,6 +58,7 @@
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
"react-router-dom": "^6.21.1", "react-router-dom": "^6.21.1",
"sonner": "^1.3.1", "sonner": "^1.3.1",

View file

@ -1,17 +1,43 @@
import { createBrowserRouter, createHashRouter, RouterProvider, type RouteObject } from 'react-router-dom' import { createBrowserRouter, createHashRouter, RouterProvider, type RouteObject } from 'react-router-dom'
import AuthCallBack from './AuthCallBack' import AuthCallBack from './AuthCallBack'
import MainLayout from './layouts/MainLayout' import MainLayout from './layouts/MainLayout'
import HomePage from './pages/Home' import HomePage from './pages/home'
import LoginPage from './pages/Login' import LoginPage from './pages/login'
import AndroidPage from './pages/Android' import AndroidPage from './pages/android'
import RecipesPage from './pages/recipes/recipes' import RecipesPage from './pages/recipes/recipes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import UploadPage from './pages/upload'
import { type MenuList } from './components/sidebar'
import { DashboardIcon, FileTextIcon, RocketIcon, UploadIcon } from '@radix-ui/react-icons'
const sideBarMenuList: MenuList = [
{
title: 'Home',
icon: DashboardIcon,
link: '/'
},
{
title: 'Recipes',
icon: FileTextIcon,
link: '/recipes'
},
{
title: 'Android',
icon: RocketIcon,
link: '/android'
},
{
title: 'Upload',
icon: UploadIcon,
link: '/upload'
}
]
function router() { function router() {
const routes: RouteObject[] = [ const routes: RouteObject[] = [
{ {
path: '/', path: '/',
element: <MainLayout />, element: <MainLayout sidebarMenu={sideBarMenuList} />,
children: [ children: [
{ {
path: '/', path: '/',
@ -24,6 +50,10 @@ function router() {
{ {
path: 'android', path: 'android',
element: <AndroidPage /> element: <AndroidPage />
},
{
path: 'upload',
element: <UploadPage />
} }
] ]
}, },

View file

@ -5,23 +5,27 @@ const AuthCallBack: React.FC = () => {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
// emit message to main process // emit message to main process
window.opener.postMessage( if (params.get('kind') === 'electron') {
{ window.location.href = import.meta.env.TAOBIN_RECIPE_MANAGER_DEEPLINK_PROTOCOL + '/login' + window.location.search
payload: 'loginSuccess', } else {
data: { window.opener.postMessage(
id: params.get('id'), {
name: params.get('name'), payload: 'loginSuccess',
email: params.get('email'), data: {
picture: params.get('picture'), id: params.get('id'),
permissions: params.get('permissions') name: params.get('name'),
} email: params.get('email'),
}, picture: params.get('picture'),
'*' permissions: params.get('permissions')
) }
},
'*'
)
window.close() window.close()
}
return <div>it just call back</div> return <div>Login Success. You can close this window now.</div>
} }
export default AuthCallBack export default AuthCallBack

View file

@ -1,29 +1,32 @@
import { DashboardIcon, FileTextIcon, RocketIcon } from '@radix-ui/react-icons' import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
const Sidebar: React.FC = () => { export type MenuList = {
title: string
icon: React.ComponentType<{ className?: string }>
link: string
}[]
interface SideBarProps {
menuList: MenuList
}
const Sidebar: React.FC<SideBarProps> = ({ menuList }) => {
return ( return (
<aside className="fixed top-0 left-0 z-40 w-64 pt-20 h-screen"> <aside className="fixed top-0 left-0 z-40 w-64 pt-20 h-screen">
<div className="h-full px-3 pb-4 overflow-y-auto bg-zinc-700"> <div className="h-full px-3 pb-4 overflow-y-auto bg-zinc-700">
<ul className="space-y-2 font-medium"> <ul className="space-y-2 font-medium">
<li> {menuList.map((item, index) => (
<Link to="/" className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500"> <li key={index}>
<DashboardIcon className="inline-block w-6 h-6 mr-3" /> <Link
Dashboard to={item.link}
</Link> className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500"
</li> >
<li> {<item.icon className="inline-block w-6 h-6 mr-3" />}
<Link to="/recipes" className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500"> {item.title}
<FileTextIcon className="inline-block w-6 h-6 mr-3" /> </Link>
Recipes </li>
</Link> ))}
</li>
<li>
<Link to="/android" className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500">
<RocketIcon className="inline-block w-6 h-6 mr-3" />
Android
</Link>
</li>
</ul> </ul>
</div> </div>
</aside> </aside>

View file

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -0,0 +1,71 @@
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import React, { type ChangeEvent, useRef } from 'react'
interface DropzoneProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
classNameWrapper?: string
className?: string
dropMessage: string
handleOnDrop: (acceptedFiles: FileList | null) => void
}
const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
({ className, classNameWrapper, dropMessage, handleOnDrop, ...props }, ref) => {
const inputRef = useRef<HTMLInputElement | null>(null)
// Function to handle drag over event
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
handleOnDrop(null)
}
// Function to handle drop event
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
const { files } = e.dataTransfer
if (inputRef.current) {
inputRef.current.files = files
handleOnDrop(files)
}
}
// Function to simulate a click on the file input element
const handleButtonClick = () => {
if (inputRef.current) {
inputRef.current.click()
}
}
return (
<Card
ref={ref}
className={cn(
`border-2 border-dashed bg-muted hover:cursor-pointer hover:border-muted-foreground/50`,
classNameWrapper
)}
>
<CardContent
className="flex flex-col items-center justify-center space-y-2 px-2 py-4 text-xs"
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleButtonClick}
>
<div className="flex items-center justify-center text-muted-foreground">
<span className="font-medium">{dropMessage}</span>
<Input
{...props}
value={undefined}
ref={inputRef}
type="file"
className={cn('hidden', className)}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleOnDrop(e.target.files)}
/>
</div>
</CardContent>
</Card>
)
}
)
export default Dropzone

View file

@ -0,0 +1,15 @@
export enum PermissionEnum {
THAI_PERMISSIONS = 1,
MALAY_PERMISSIONS = 1 << 1,
AUS_PERMISSIONS = 1 << 2,
ALPHA3_PERMISSIONS = 1 << 3,
VIEWER_PERMISSIONS = 1 << 4,
EDITOR_PERMISSIONS = VIEWER_PERMISSIONS << 1,
SUPER_ADMIN = THAI_PERMISSIONS |
MALAY_PERMISSIONS |
AUS_PERMISSIONS |
ALPHA3_PERMISSIONS |
(VIEWER_PERMISSIONS | EDITOR_PERMISSIONS)
}

View file

@ -0,0 +1,15 @@
import { CheckCircledIcon, CrossCircledIcon, QuestionMarkCircledIcon } from '@radix-ui/react-icons'
export enum RecipeStatus {
DISABLE = 'DISABLE',
ENABLE = 'ENABLE'
}
const statusIcons = {
[RecipeStatus.DISABLE]: CrossCircledIcon,
[RecipeStatus.ENABLE]: CheckCircledIcon
}
export const getRecipeStatusIcon = (status: RecipeStatus) => {
return statusIcons[status] || QuestionMarkCircledIcon
}

View file

@ -0,0 +1,16 @@
import customAxios from '@/lib/customAxios'
import { type RecipeOverview } from '@/models/recipe/schema'
interface GetRecipeOverviewFilterQuery {
countryID: string
filename: string
materialIDs: string
}
export const getRecipeOverview = (query?: GetRecipeOverviewFilterQuery): Promise<RecipeOverview[]> => {
return customAxios
.get<RecipeOverview[]>(import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/recipe/overview', { params: query })
.then(res => {
return res.data
})
}

View file

@ -1,18 +1,11 @@
import { create } from 'zustand' import { create } from 'zustand'
import customAxios from '../lib/customAxios' import customAxios from '../lib/customAxios'
import type { User } from '@/models/user/schema'
export interface UserInfo {
id: string
name: string
email: string
picture: string
permissions: string[]
}
interface UserAuth { interface UserAuth {
userInfo: UserInfo | null userInfo: User | null
setUserInfo: (userInfo: UserInfo | null) => void setUserInfo: (userInfo: User | null) => void
getUserInfo: () => Promise<UserInfo | null> getUserInfo: () => Promise<User | null>
logout: () => void logout: () => void
} }
@ -21,7 +14,7 @@ const userAuthStore = create<UserAuth>(set => ({
setUserInfo: userInfo => set({ userInfo }), setUserInfo: userInfo => set({ userInfo }),
getUserInfo: () => { getUserInfo: () => {
return customAxios return customAxios
.get<UserInfo>('/user/me') .get<User>('/user/me')
.then(res => { .then(res => {
set({ userInfo: res.data }) set({ userInfo: res.data })
return res.data return res.data

View file

@ -1,12 +1,16 @@
import { Outlet, useNavigate } from 'react-router-dom' import { Outlet, useNavigate } from 'react-router-dom'
import Navbar from '../components/Header' import Navbar from '../components/header'
import Sidebar from '../components/Sidebar' import Sidebar, { type MenuList } from '../components/sidebar'
import userAuthStore from '../hooks/userAuth' import userAuthStore from '../hooks/userAuth'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { useCallback } from 'react' import { useCallback } from 'react'
import { Toaster } from '@/components/ui/toaster' import { Toaster } from '@/components/ui/toaster'
const MainLayout = () => { interface MainLayoutProps {
sidebarMenu: MenuList
}
const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
const { userInfo, getUserInfo } = userAuthStore( const { userInfo, getUserInfo } = userAuthStore(
useShallow(state => ({ useShallow(state => ({
userInfo: state.userInfo, userInfo: state.userInfo,
@ -33,7 +37,7 @@ const MainLayout = () => {
return ( return (
<> <>
<Navbar /> <Navbar />
<Sidebar /> <Sidebar menuList={sidebarMenu} />
<main className="p-8 sm:ml-64 mt-20"> <main className="p-8 sm:ml-64 mt-20">
<Outlet /> <Outlet />
<Toaster /> <Toaster />

View file

@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
const customAxios = axios.create({ const customAxios = axios.create({
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL, baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
withCredentials: true withCredentials: true
}) })

View file

@ -0,0 +1,32 @@
import { PermissionEnum } from '@/constants/permissions'
export function getPermissions(permissions: number): PermissionEnum[] {
const permissionsArray: PermissionEnum[] = []
if (permissions & PermissionEnum.THAI_PERMISSIONS) {
permissionsArray.push(PermissionEnum.THAI_PERMISSIONS)
}
if (permissions & PermissionEnum.MALAY_PERMISSIONS) {
permissionsArray.push(PermissionEnum.MALAY_PERMISSIONS)
}
if (permissions & PermissionEnum.AUS_PERMISSIONS) {
permissionsArray.push(PermissionEnum.AUS_PERMISSIONS)
}
if (permissions & PermissionEnum.ALPHA3_PERMISSIONS) {
permissionsArray.push(PermissionEnum.ALPHA3_PERMISSIONS)
}
if (permissions & PermissionEnum.VIEWER_PERMISSIONS) {
permissionsArray.push(PermissionEnum.VIEWER_PERMISSIONS)
}
if (permissions & PermissionEnum.EDITOR_PERMISSIONS) {
permissionsArray.push(PermissionEnum.EDITOR_PERMISSIONS)
}
return permissionsArray
}
export function hasPermission(permissions: number, permission: PermissionEnum): boolean {
return Boolean(permissions & permission)
}
export function permissionsToNumber(permissions: PermissionEnum[]): number {
return permissions.reduce((acc, permission) => acc | permission, 0)
}

View file

@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from 'clsx'
import { twMerge } from "tailwind-merge" import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))

View file

@ -19,10 +19,7 @@ if (window.electronRuntime) {
}) })
window.ipcRenderer window.ipcRenderer
.invoke( .invoke('keyChainSync', import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME)
'keyChainSync',
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME
)
.then(result => { .then(result => {
console.log(result) console.log(result)
}) })

View file

@ -0,0 +1,20 @@
import { RecipeStatus } from '@/constants/recipe'
import { z } from 'zod'
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string()
})
export type Task = z.infer<typeof taskSchema>
export const recipeOverviewSchema = z.object({
productCode: z.string(),
name: z.string(),
otherName: z.string(),
status: z.nativeEnum(RecipeStatus),
lastUpdated: z.date()
})
export type RecipeOverview = z.infer<typeof recipeOverviewSchema>

View file

@ -0,0 +1,10 @@
import { z } from 'zod'
export const userSchema = z.object({
name: z.string(),
email: z.string().email(),
picture: z.string().url(),
permissions: z.number()
})
export type User = z.infer<typeof userSchema>

View file

@ -82,7 +82,9 @@ const AndroidPage: React.FC = () => {
) )
async function scrcpyConnect() { async function scrcpyConnect() {
const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res => res.arrayBuffer()) const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
res.arrayBuffer()
)
await AdbScrcpyClient.pushServer( await AdbScrcpyClient.pushServer(
adb!, adb!,
@ -94,7 +96,9 @@ const AndroidPage: React.FC = () => {
}) })
) )
const res = await adb!.subprocess.spawn('CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25') const res = await adb!.subprocess.spawn(
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
)
res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo( res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
new WritableStream({ new WritableStream({
@ -111,7 +115,12 @@ const AndroidPage: React.FC = () => {
control: true, control: true,
logLevel: ScrcpyLogLevel1_18.Debug logLevel: ScrcpyLogLevel1_18.Debug
}) })
const _client = await AdbScrcpyClient.start(adb!, '/data/local/tmp/scrcpy-server.jar', '1.25', new AdbScrcpyOptions1_22(scrcpyOption)) const _client = await AdbScrcpyClient.start(
adb!,
'/data/local/tmp/scrcpy-server.jar',
'1.25',
new AdbScrcpyOptions1_22(scrcpyOption)
)
const videoStream: AdbScrcpyVideoStream | undefined = await _client?.videoStream const videoStream: AdbScrcpyVideoStream | undefined = await _client?.videoStream
@ -241,7 +250,7 @@ const AndroidPage: React.FC = () => {
} }
return ( return (
<div className="flex justify-center items-center mx-auto"> <div className="flex flex-col justify-center items-center mx-auto">
<div className="flex justify-around items-start mx-auto w-full h-full"> <div className="flex justify-around items-start mx-auto w-full h-full">
<div> <div>
<div ref={screenRef} className="min-h-[700px] min-w-[400px] max-h-[700px] max-w-[400px] bg-gray-700"></div> <div ref={screenRef} className="min-h-[700px] min-w-[400px] max-h-[700px] max-w-[400px] bg-gray-700"></div>

View file

@ -1,13 +1,13 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import googleLogo from '../assets/google-color.svg' import googleLogo from '@/assets/google-color.svg'
import userAuthStore, { type UserInfo } from '../hooks/userAuth' import userAuthStore from '@/hooks/userAuth'
import { userSchema } from '@/models/user/schema'
const LoginPage: React.FC = () => { const LoginPage: React.FC = () => {
const setUserInfo = userAuthStore(state => state.setUserInfo) const setUserInfo = userAuthStore(state => state.setUserInfo)
const navigate = useNavigate() const navigate = useNavigate()
const redirectUrl = const redirectUrl = new URLSearchParams(window.location.search).get('redirect_to') ?? '/'
new URLSearchParams(window.location.search).get('redirect_to') ?? '/'
const loginWithGoogle = () => { const loginWithGoogle = () => {
// if is web mode then use window.open // if is web mode then use window.open
@ -15,24 +15,35 @@ const LoginPage: React.FC = () => {
if (window.electronRuntime) { if (window.electronRuntime) {
window.ipcRenderer.send( window.ipcRenderer.send(
'deeplink', 'deeplink',
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl + '&kind=electron'
'/auth/google?redirect_to=' +
redirectUrl +
'&kind=electron'
) )
window.ipcRenderer.on('loginSuccess', (_event, data) => { window.ipcRenderer.on('loginSuccess', (_event, data) => {
console.log(data) console.log(data)
setUserInfo(data satisfies UserInfo) const { access_token, max_age, refresh_token } = data
window.ipcRenderer.send('set-keyChain', {
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
password: access_token + ';' + max_age
})
window.ipcRenderer.send('set-keyChain', {
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
password: refresh_token
})
data.permissions = Number(data.permissions)
const user = userSchema.parse(data)
setUserInfo(user)
navigate(redirectUrl) navigate(redirectUrl)
}) })
} else { } else {
// open new window and listen to message from window.opener // open new window and listen to message from window.opener
window.open( window.open(
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl,
'/auth/google?redirect_to=' +
redirectUrl,
'_blank', '_blank',
'width=500,height=600' 'width=500,height=600'
) )
@ -40,22 +51,8 @@ const LoginPage: React.FC = () => {
// listen to message from new window // listen to message from new window
window.addEventListener('message', event => { window.addEventListener('message', event => {
if (event.data.payload === 'loginSuccess') { if (event.data.payload === 'loginSuccess') {
// const { access_token, max_age, refresh_token } = event.data.data const user = userSchema.parse(event.data.data)
// setPassword( setUserInfo(user)
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
// import.meta.env
// .TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
// access_token + ';' + max_age
// )
// setPassword(
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
// import.meta.env
// .TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
// refresh_token
// )
setUserInfo(event.data.data satisfies UserInfo)
navigate(redirectUrl) navigate(redirectUrl)
} }
@ -64,17 +61,12 @@ const LoginPage: React.FC = () => {
} }
return ( return (
<div className="flex flex-col items-center justify-center h-screen"> <div className="flex h-screen flex-col items-center justify-center">
<button <button
onClick={loginWithGoogle} onClick={loginWithGoogle}
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150" className="flex gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-slate-700 transition duration-150 hover:border-slate-400 hover:text-slate-900 hover:shadow"
> >
<img <img className="h-6 w-6" src={googleLogo} alt="google logo" loading="eager" />
className="w-6 h-6"
src={googleLogo}
alt="google logo"
loading="eager"
/>
<span>Login with @forth.co.th Google account</span> <span>Login with @forth.co.th Google account</span>
</button> </button>
</div> </div>

View file

@ -1,12 +1,12 @@
import { type ColumnDef } from '@tanstack/react-table' import { type ColumnDef } from '@tanstack/react-table'
import { type Task } from '../models/schema' import { type RecipeOverview } from '@/models/recipe/schema'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import DataTableColumnHeader from './data-table-column-header' import DataTableColumnHeader from './data-table-column-header'
import { labels, priorities, statuses } from '../models/data'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import DataTableRowActions from './data-table-row-actions' import DataTableRowActions from './data-table-row-actions'
import { type RecipeStatus, getRecipeStatusIcon } from '@/constants/recipe'
export const columns: ColumnDef<Task>[] = [ export const columns: ColumnDef<RecipeOverview>[] = [
{ {
id: 'select', id: 'select',
header: ({ table }) => ( header: ({ table }) => (
@ -29,22 +29,22 @@ export const columns: ColumnDef<Task>[] = [
enableHiding: false enableHiding: false
}, },
{ {
accessorKey: 'id', accessorKey: 'productCode',
header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />, header: ({ column }) => <DataTableColumnHeader column={column} title="ProductCode" />,
cell: ({ row }) => <div className="w-[80px]">{row.getValue('id')}</div>, cell: ({ row }) => <div className="w-[80px]">{row.getValue('productCode')}</div>,
enableSorting: false, enableSorting: false,
enableHiding: false enableHiding: false
}, },
{ {
accessorKey: 'title', accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />, header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
cell: ({ row }) => { cell: ({ row }) => {
const label = labels.find(label => label.value === row.original.label) const label = { label: 'Test Label' } // labels.find(label => label.value === row.getValue('label'))
return ( return (
<div className="flex space-x-2"> <div className="flex space-x-2">
{label && <Badge variant="outline">{label.label}</Badge>} {label && <Badge variant="outline">{label.label}</Badge>}
<span className="max-w-[500px] truncate font-medium">{row.getValue('title')}</span> <span className="max-w-[500px] truncate font-medium">{row.getValue('name')}</span>
</div> </div>
) )
} }
@ -53,16 +53,16 @@ export const columns: ColumnDef<Task>[] = [
accessorKey: 'status', accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />, header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => { cell: ({ row }) => {
const status = statuses.find(status => status.value === row.getValue('status')) const status: RecipeStatus = row.getValue('status')
const StatusIcon = getRecipeStatusIcon(status)
if (!status) { if (!status) {
return null return null
} }
return ( return (
<div className="flex w-[100px] items-center"> <div className="flex w-[100px] items-center">
{status.icon && <status.icon className="mr-2 h-4 w-4 text-muted-foreground" />} {<StatusIcon className="text-muted-foreground mr-2 h-4 w-4" />}
<span>{status.label}</span> <span>{}</span>
</div> </div>
) )
}, },
@ -71,19 +71,12 @@ export const columns: ColumnDef<Task>[] = [
} }
}, },
{ {
accessorKey: 'priority', accessorKey: 'lastUpdated',
header: ({ column }) => <DataTableColumnHeader column={column} title="Priority" />, header: ({ column }) => <DataTableColumnHeader column={column} title="Last Updated" />,
cell: ({ row }) => { cell: ({ row }) => {
const priority = priorities.find(priority => priority.value === row.getValue('priority'))
if (!priority) {
return null
}
return ( return (
<div className="flex items-center"> <div className="flex items-center">
{priority.icon && <priority.icon className="mr-2 h-4 w-4 text-muted-foreground" />} <span>{row.getValue('lastUpdated')}</span>
<span>{priority.label}</span>
</div> </div>
) )
}, },

View file

@ -1,5 +1,5 @@
import { type Row } from '@tanstack/react-table' import { type Row } from '@tanstack/react-table'
import { taskSchema } from '../models/schema' import { taskSchema } from '@/models/recipe/schema'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -15,7 +15,7 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DotsHorizontalIcon } from '@radix-ui/react-icons' import { DotsHorizontalIcon } from '@radix-ui/react-icons'
import { labels } from '../models/data' import { labels } from '@/models/data'
interface DataTableRowActionsProps<TData> { interface DataTableRowActionsProps<TData> {
row: Row<TData> row: Row<TData>
@ -27,7 +27,7 @@ const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) =
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"> <Button variant="ghost" className="data-[state=open]:bg-muted flex h-8 w-8 p-0">
<DotsHorizontalIcon className="h-4 w-4" /> <DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
</Button> </Button>

View file

@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { type Table } from '@tanstack/react-table' import { type Table } from '@tanstack/react-table'
import { statuses, priorities } from '../models/data' import { statuses, priorities } from '@/models/data'
import DataTableFacetedFilter from './data-table-faceted-filter' import DataTableFacetedFilter from './data-table-faceted-filter'
import DataTableViewOptions from './data-table-view-options' import DataTableViewOptions from './data-table-view-options'
import { Cross2Icon } from '@radix-ui/react-icons' import { Cross2Icon } from '@radix-ui/react-icons'
@ -15,7 +15,7 @@ const DataTableToolbar = <TData,>({ table }: DataTableToolbarProps<TData>) => {
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col w-full rounded-lg bg-white shadow-md p-3 space-y-4"> <div className="flex w-full flex-col space-y-4 rounded-lg bg-white p-3 shadow-md">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2"> <div className="flex flex-1 items-center space-x-2">
<Input <Input

View file

@ -1,11 +0,0 @@
import { z } from 'zod'
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string()
})
export type Task = z.infer<typeof taskSchema>

View file

@ -1,30 +1,21 @@
import { type Task, taskSchema } from './models/schema'
import { z } from 'zod'
import DataTable from './components/data-table'
import { columns } from './components/columns'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { columns } from './components/columns'
// Simulate a database read for tasks. import DataTable from './components/data-table'
async function getTasks() { import { getRecipeOverview } from '@/hooks/recipe/get-recipe-overview'
const data = await fetch(new URL('./models/tasks.json', import.meta.url)).then(res => res.text())
const tasks = JSON.parse(data.toString())
return z.array(taskSchema).parse(tasks)
}
const RecipesPage = () => { const RecipesPage = () => {
const { data: tasks, isLoading } = useQuery({ queryKey: ['tasks'], queryFn: getTasks }) const { data: recipeOverviewList, isLoading } = useQuery({
queryKey: ['recipe-overview'],
console.log('here') queryFn: () => getRecipeOverview()
})
return ( return (
<div className="flex flex-col w-full gap-3"> <div className="flex w-full flex-col gap-3">
<section> <section>
<h1 className="text-3xl font-bold text-gray-900">Recipes</h1> <h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
</section> </section>
<section> <section>
<DataTable data={tasks} columns={columns} isLoading={isLoading} /> <DataTable data={recipeOverviewList ?? []} columns={columns} isLoading={isLoading} />
</section> </section>
</div> </div>
) )

View file

@ -0,0 +1,46 @@
import { useEffect, useState } from 'react'
import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import GoldenRetriever from '@uppy/golden-retriever'
import Tus from '@uppy/tus'
import '@uppy/core/dist/style.min.css'
import '@uppy/dashboard/dist/style.min.css'
import axios from 'axios'
const UploadPage: React.FC = () => {
const [uppy] = useState(() =>
new Uppy().use(GoldenRetriever, { serviceWorker: true }).use(Tus, {
endpoint: import.meta.env.TAOBIN_RECIPE_MANAGER_TUS_SERVER_URL ?? 'http://localhost:8090/files/',
withCredentials: true
})
)
const [files, setFiles] = useState([])
async function getFiles() {
const files = await axios.get('http://localhost:8090/list').then(res => res.data)
console.log(files)
setFiles(files)
}
useEffect(() => {
getFiles()
}, [])
uppy.on('upload-success', () => {
getFiles()
})
return (
<div className="flex justify-between items-center">
<Dashboard uppy={uppy} proudlyDisplayPoweredByUppy={false} showProgressDetails={true} />
<div className="flex flex-col">
{files.map((file, index) => (
<div key={index}>{JSON.stringify(file)}</div>
))}
</div>
</div>
)
}
export default UploadPage

View file

@ -2,6 +2,7 @@
interface ImportMetaEnv { interface ImportMetaEnv {
TAOBIN_RECIPE_MANAGER_SERVER_URL: string TAOBIN_RECIPE_MANAGER_SERVER_URL: string
TAOBIN_RECIPE_MANAGER_TUS_SERVER_URL: string
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME: string TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME: string
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN: string TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN: string
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN: string TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN: string

3
server/.gitignore vendored
View file

@ -4,4 +4,5 @@
token.json token.json
client_secret.json client_secret.json
app.env app.env
cofffeemachineConfig cofffeemachineConfig
/uploads/*

@ -1 +1 @@
Subproject commit 85d4ada5c95a77803281ef76b1421807d9203353 Subproject commit 6e0d512afc4667a2204ff773ad8a3851120cdadd

View file

@ -1,7 +1,8 @@
package config package config
type ServerConfig struct { type ServerConfig struct {
ServerPort uint `mapstructure:"SERVER_PORT"` ServerPort int `mapstructure:"SERVER_PORT"`
TusServerPort int `mapstructure:"TUS_SERVER_PORT"`
AllowedOrigins string `mapstructure:"ALLOWED_ORIGINS"` AllowedOrigins string `mapstructure:"ALLOWED_ORIGINS"`
ClientRedirectURL string `mapstructure:"CLIENT_REDIRECT_URL"` ClientRedirectURL string `mapstructure:"CLIENT_REDIRECT_URL"`
ClientElectronRedirectURL string `mapstructure:"CLIENT_ELECTRON_REDIRECT_URL"` ClientElectronRedirectURL string `mapstructure:"CLIENT_ELECTRON_REDIRECT_URL"`

Binary file not shown.

View file

@ -5,26 +5,31 @@ go 1.21.1
require ( require (
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.18 github.com/mattn/go-sqlite3 v1.14.18
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
golang.org/x/oauth2 v0.12.0 github.com/tus/tusd/v2 v2.2.2
google.golang.org/api v0.143.0 golang.org/x/oauth2 v0.14.0
google.golang.org/api v0.152.0
) )
require ( require (
cloud.google.com/go/compute v1.23.0 // indirect github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
)
require (
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.4.0 github.com/google/uuid v1.4.0
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.13.0 // indirect golang.org/x/crypto v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.57.0 // indirect google.golang.org/grpc v1.59.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
) )
@ -44,9 +49,9 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

View file

@ -23,8 +23,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@ -40,9 +40,13 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Acconut/go-httptest-recorder v1.0.0 h1:TAv2dfnqp/l+SUvIaMAUK4GeN4+wqb6KZsFFFTGhoJg=
github.com/Acconut/go-httptest-recorder v1.0.0/go.mod h1:CwQyhTH1kq/gLyWiRieo7c0uokpu3PXeyF/nZjUNtmM=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 h1:y4B3+GPxKlrigF1ha5FFErxK+sr6sWxQovRMzwMhejo=
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -86,6 +90,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -116,8 +122,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -137,15 +143,13 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -185,8 +189,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
@ -207,10 +211,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tus/tusd/v2 v2.2.2 h1:urJ0Is3ew3OWdWF6ZkaTEfJe3c/sFINCkWsK0/73m+k=
github.com/tus/tusd/v2 v2.2.2/go.mod h1:XNEk33RSdHtrqVMVKJhoRxw97MYPIbFC78sZWwfqJ0M=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -236,8 +243,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -248,6 +255,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -304,8 +313,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -315,8 +324,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -327,8 +336,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -365,8 +374,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -376,8 +385,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -451,8 +460,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.143.0 h1:o8cekTkqhywkbZT6p1UHJPZ9+9uuCAJs/KYomxZB8fA= google.golang.org/api v0.152.0 h1:t0r1vPnfMc260S2Ci+en7kfCZaLOPs5KI0sVV/6jZrY=
google.golang.org/api v0.143.0/go.mod h1:FoX9DO9hT7DLNn97OuoZAGSDuNAXdJRuGK98rSUgurk= google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -497,12 +506,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -519,8 +528,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -6,20 +6,38 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"recipe-manager/config"
"recipe-manager/services/oauth"
"syscall" "syscall"
"time" "time"
"github.com/spf13/viper"
) )
func main() { func main() {
s := NewServer()
config, err := loadConfig(".")
if err != nil {
// TODO: use default config instead
log.Fatal(err)
}
oauthService := oauth.NewOAuthService(config)
server := NewServer(config, oauthService)
tusServer := NewTusServer(config, oauthService)
serverCtx, serverStopCtx := context.WithCancel(context.Background()) serverCtx, serverStopCtx := context.WithCancel(context.Background())
tusServerCtx, tusServerStopCtx := context.WithCancel(context.Background())
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() { go func() {
<-sig <-sig
shutdownCtx, cancel := context.WithTimeout(serverCtx, 30*time.Second) shutdownCtx, cancel := context.WithTimeout(serverCtx, 30*time.Second)
tusShutdownCtx, tusShutdownCancel := context.WithTimeout(tusServerCtx, 30*time.Second)
go func() { go func() {
<-shutdownCtx.Done() <-shutdownCtx.Done()
@ -29,20 +47,66 @@ func main() {
} }
}() }()
err := s.Shutdown(shutdownCtx) go func() {
<-tusShutdownCtx.Done()
if errors.Is(tusShutdownCtx.Err(), context.DeadlineExceeded) {
log.Println("Tus server shutdown timeout, force exit")
tusShutdownCancel()
}
}()
err := server.Shutdown(shutdownCtx)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
err = tusServer.Shutdown(tusShutdownCtx)
if err != nil {
log.Fatal(err)
}
tusServerStopCtx()
serverStopCtx() serverStopCtx()
}() }()
// Spawn a goroutine to run git pull every 10 minutes // Spawn a goroutine to run git pull every 10 minutes
//go RunGitRecipeWorker() // ** Use crontab instead ** //go RunGitRecipeWorker() // ** Use crontab instead **
err := s.Run() go func() {
if err != nil { err := tusServer.Run()
log.Fatal(err) if err != nil {
} log.Fatal(err)
}
}()
go func() {
err := server.Run()
if err != nil {
log.Fatal(err)
}
}()
<-tusServerCtx.Done()
<-serverCtx.Done() <-serverCtx.Done()
} }
func loadConfig(path string) (*config.ServerConfig, error) {
viper.AddConfigPath(path)
viper.SetConfigName("app")
viper.SetConfigType("env")
viper.AutomaticEnv()
var serverConfig config.ServerConfig
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
err = viper.Unmarshal(&serverConfig)
if err != nil {
return nil, err
}
return &serverConfig, nil
}

View file

@ -23,31 +23,9 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
) )
func loadConfig(path string) (*config.ServerConfig, error) {
viper.AddConfigPath(path)
viper.SetConfigName("app")
viper.SetConfigType("env")
viper.AutomaticEnv()
var serverConfig config.ServerConfig
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
err = viper.Unmarshal(&serverConfig)
if err != nil {
return nil, err
}
return &serverConfig, nil
}
type Server struct { type Server struct {
server *http.Server server *http.Server
data *data.Data data *data.Data
@ -57,22 +35,17 @@ type Server struct {
taoLogger *logger.TaoLogger taoLogger *logger.TaoLogger
} }
func NewServer() *Server { func NewServer(cfg *config.ServerConfig, oauthService oauth.OAuthService) *Server {
serverCfg, err := loadConfig(".") taoLogger := logger.NewTaoLogger(cfg)
taoLogger.Log = taoLogger.Log.Named("Server")
if err != nil {
log.Fatal(err)
}
taoLogger := logger.NewTaoLogger(serverCfg)
return &Server{ return &Server{
server: &http.Server{Addr: fmt.Sprintf(":%d", serverCfg.ServerPort)}, server: &http.Server{Addr: fmt.Sprintf(":%d", cfg.ServerPort)},
data: data.NewData(taoLogger), data: data.NewData(taoLogger),
database: data.NewSqliteDatabase(), database: data.NewSqliteDatabase(),
cfg: serverCfg, cfg: cfg,
oauth: oauth.NewOAuthService(serverCfg), oauth: oauthService,
taoLogger: taoLogger, taoLogger: taoLogger,
} }
} }

135
server/tus_server.go Normal file
View file

@ -0,0 +1,135 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"recipe-manager/config"
"recipe-manager/data"
"recipe-manager/helpers"
"recipe-manager/services/logger"
"recipe-manager/services/oauth"
"regexp"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/tus/tusd/v2/pkg/filestore"
tusd "github.com/tus/tusd/v2/pkg/handler"
"go.uber.org/zap"
)
type TusServer struct {
cfg *config.ServerConfig
oauth oauth.OAuthService
server *http.Server
handler *tusd.Handler
taoLogger *logger.TaoLogger
database *sqlx.DB
}
func NewTusServer(cfg *config.ServerConfig, oauth oauth.OAuthService) *TusServer {
taoLogger := logger.NewTaoLogger(cfg)
taoLogger.Log = taoLogger.Log.Named("TusServer")
store := filestore.FileStore{
Path: "./uploads",
}
composer := tusd.NewStoreComposer()
store.UseIn(composer)
handler, err := tusd.NewHandler(tusd.Config{
BasePath: "/files/",
StoreComposer: composer,
NotifyCompleteUploads: true,
Cors: &tusd.CorsConfig{
AllowOrigin: regexp.MustCompile(strings.Replace(cfg.AllowedOrigins, ",", "|", -1)),
AllowCredentials: true,
AllowMethods: tusd.DefaultCorsConfig.AllowMethods,
AllowHeaders: tusd.DefaultCorsConfig.AllowHeaders,
ExposeHeaders: tusd.DefaultCorsConfig.ExposeHeaders,
MaxAge: tusd.DefaultCorsConfig.MaxAge,
},
})
if err != nil {
panic(fmt.Errorf("unable to create handler: %s", err))
}
go func() {
for {
event := <-handler.CompleteUploads
fmt.Printf("Upload %s finished\n", event.Upload.ID)
}
}()
return &TusServer{
server: &http.Server{Addr: ":" + strconv.Itoa(int(cfg.TusServerPort))},
cfg: cfg,
handler: handler,
taoLogger: taoLogger,
oauth: oauth,
database: data.NewSqliteDatabase(),
}
}
func (ts *TusServer) Run() error {
mux := http.NewServeMux()
ts.taoLogger.Log.Info("Server running", zap.String("addr", ts.server.Addr))
// User Service
// userService := user.NewUserService(ts.cfg, ts.database, ts.taoLogger)
mux.Handle("/files/", http.StripPrefix("/files/", ts.handler))
mux.HandleFunc("/list", listFiles)
port := strconv.Itoa(int(ts.cfg.TusServerPort))
if port == "" {
port = "8090"
}
return http.ListenAndServe(":"+strconv.Itoa(int(ts.cfg.TusServerPort)), mux)
}
func (ts *TusServer) Shutdown(ctx context.Context) error {
return ts.server.Shutdown(ctx)
}
func listFiles(w http.ResponseWriter, r *http.Request) {
// read all file in ./uploads/*.info file
// unmarshal json and return the list of files
// add cors
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
files := helpers.ListFile("./uploads/*.info")
var result []interface{}
for _, file := range files {
fileContent, err := helpers.ReadFile(file)
if err != nil {
fmt.Println(err)
continue
}
var data interface{}
if err := json.Unmarshal([]byte(fileContent), &data); err != nil {
fmt.Println(err)
}
result = append(result, data)
}
json.NewEncoder(w).Encode(result)
}