Seize the day

SEARCH RESAULT : 글 검색 결과 - 전체 글 (총 490개)

POST : Backend study

Honojs file logger 초간단 구현

https://stackoverflow.com/questions/32719923/redirecting-stdout-to-file-nodejs 를 참고했다. 

custom-logger.ts

import { MiddlewareHandler } from "hono/types"
const fs = require('fs')

const isDebug: Boolean = process.env.NODE_ENV !== "production";

var myLogFileStream = fs.createWriteStream('./access.log', {flags: 'a'}); //for append 
// TODO create <date>.log
var fileLogger = new console.Console(myLogFileStream, myLogFileStream);


export const loggerFunc = (tag:string, message: any, ...rest: any[]) => {
    const datetime = new Date().toLocaleString()

    fileLogger.log(tag, datetime, message, ...rest);

    if (isDebug) { 
        console.log(tag, datetime, message, ...rest);
    }
  }

process.on('uncaughtException', function(err) {
    const datetime = new Date().toLocaleString()

    fileLogger.error("[uncaughtException]", datetime, (err && err.stack) ? err.stack : err);
    
    if (isDebug) { 
        console.error("[uncaughtException]", datetime, (err && err.stack) ? err.stack : err);
    }
});


export const customLogger = (): MiddlewareHandler => {
    return async function logger(c, next) {
      const { method, path } = c.req
      const userAgent = c.req.header('user-agent')
      const ip = c.env.requestIP(c.req.raw).address      
      const datetime = new Date().toLocaleString()
      
      await next()
      
      fileLogger.log("[Request]", datetime, method, path, c.res.status, ip, userAgent)

      if (isDebug) { 
          console.log("[Request]", datetime, method, path, c.res.status, ip, userAgent)
      }        
    }
  }

loggerFunc으로 log를 찍을경우 파일로 쓰기가 가능하다.  무한히 log가 커지지 않도록 주기적으로 삭제하거나, 날짜별로 새로 생성하는 것은 고민 필요,  log Level에 따라서 여러 파일로 분기하는 것도 고민 필요

 

index.ts

import { customLogger, loggerFunc } from './custom-logger';


const limiter = rateLimiter({
  windowMs: 6 * 1000, 
  limit: 5, // Limit each IP to 100 requests per `window`
  standardHeaders: "draft-6",
  keyGenerator: (c) => { 
    const ip = c.env.requestIP(c.req.raw).address;
    return ip;
  }, // Method to generate custom identifiers for clients.
  handler: async (c, _, options) => {
    // TODO: 이겨서 ip를 저장해 두었다가 1시간 동안 request 블럭시키는 것도 가능할 듯.
    
    // 로그가 안 찍혀서 직접 찍는다. 
    const { method, path } = c.req
    const userAgent = c.req.header('user-agent')
    const ip = c.env.requestIP(c.req.raw).address      
    const datetime = new Date().toLocaleString()    
    loggerFunc("[rateLim]", datetime, method, path, options.statusCode, ip, userAgent)

    // Json으로 응답하자
    return c.json({code: 429, message: "block too many requests"}, options.statusCode)
  }
});


app.onError((err, c) => {
  if (err instanceof ErrorCodeException) {
    loggerFunc("[onError]", `ErrorCodeException: ${err.code} ${err.message}`); 
    return c.json({code: err.code, message: err.message}, 500);
  }
  if (err instanceof HTTPException) {
    // Get the custom response
    loggerFunc("[onError]", `HTTPException: ${err.message}`); 
    return c.json({code: err.status, message: err.message}, err.status);
  }
  //...
})

 

실행 결과

dajkim76@Kims-Mac-mini ~/test/hono_test % cat access.log
[Request] 7/13/2024, 4:41:01 AM GET /aa 404 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[Request] 7/13/2024, 4:41:02 AM GET /aa 404 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[Request] 7/13/2024, 4:41:02 AM GET /aa 404 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[Request] 7/13/2024, 4:41:03 AM GET /aa 404 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[Request] 7/13/2024, 4:41:03 AM GET /aa 404 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:03 AM 7/13/2024, 4:41:03 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:04 AM 7/13/2024, 4:41:04 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:04 AM 7/13/2024, 4:41:04 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:04 AM 7/13/2024, 4:41:04 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:05 AM 7/13/2024, 4:41:05 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:05 AM 7/13/2024, 4:41:05 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:05 AM 7/13/2024, 4:41:05 AM GET /aa 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:15 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:15 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:19 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:19 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:20 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:20 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:21 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:21 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:21 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:21 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[rateLim] 7/13/2024, 4:41:21 AM 7/13/2024, 4:41:21 AM GET / 429 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:22 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:22 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:22 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:22 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:22 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:22 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:22 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:22 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
[onError] 7/13/2024, 4:41:22 AM ErrorCodeException: 1234 error remote ip ::1
[Request] 7/13/2024, 4:41:22 AM GET / 500 ::1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36

 

=== 7/14일 추가===

log 파일을 날짜별로 생성하고,  7일이 지난 log파일은 삭제하는 코드를 만들었다. 간단하게 만들었다. 이 보다 더 간단하게는 힘들 것 같다. 

custom-logger.ts 전체코드

import { MiddlewareHandler } from "hono/types"
const fs = require('node:fs')

const isDebug: Boolean = process.env.NODE_ENV !== "production";
const LOG_MAX_DAYS = 7;         // 7일이 지난 파일은 삭제
const logDir = "./logs"
let logDateString: string;
let logFileStream: NodeJS.WritableStream
let fileLogger: Console

function getDateString(date: Date) {
    const year = date.getFullYear().toString();
    let month = (date.getMonth() + 1).toString();
    let day = date.getDate().toString();

    if (month.length == 1) month = `0${month}`;
    if (day.length == 1) day = `0${day}`
    return `${year}-${month}-${day}`;    
}

function checkLogFile() {
    const nowDate = new Date();
    const dateString = getDateString(nowDate);
    if (logDateString !== dateString) {        
        if (!fs.existsSync(logDir)) {
            try {
                fs.mkdirSync(logDir);                
            } catch(e) {
                if(e.code !== 'EEXIST'){
                    throw e;
                }
            }
        }

        if (logFileStream) {
            logFileStream.end();
        }
        
        const logPath = logDir + "/" + dateString + ".log";
        logFileStream = fs.createWriteStream(logPath, {flags: 'a'});
        fileLogger = new console.Console(logFileStream, logFileStream);
        logDateString = dateString;
        console.log(`log path: ${logPath}`);

        // remove old log file
        const oldDate = new Date(nowDate);
        oldDate.setDate(nowDate.getDate() - LOG_MAX_DAYS);
        const oldDateString = getDateString(oldDate);
        const oldLogPath = logDir + "/" + oldDateString + ".log";
        console.log(`old log path: ${oldLogPath}`);
        if (fs.existsSync(oldLogPath)) {
            try {
                fs.unlinkSync(oldLogPath);
            } catch (err) {
              console.error(err);
            }
        }        
    }
}

checkLogFile();

export const loggerFunc = (tag:string, message: any, ...rest: any[]) => {
    const datetime = new Date().toLocaleString()

    checkLogFile();
    fileLogger.log(tag, datetime, message, ...rest);

    if (isDebug) { 
        console.log(tag, datetime, message, ...rest);
    }
  }

process.on('uncaughtException', function(err) {
    const datetime = new Date().toLocaleString()

    checkLogFile();
    fileLogger.error("[uncaughtException]", datetime, (err && err.stack) ? err.stack : err);
    
    if (isDebug) { 
        console.error("[uncaughtException]", datetime, (err && err.stack) ? err.stack : err);
    }
});


export const customLogger = (): MiddlewareHandler => {
    return async function logger(c, next) {
      const { method, path } = c.req
      const userAgent = c.req.header('user-agent')
      const ip = c.env.requestIP(c.req.raw).address      
      const datetime = new Date().toLocaleString()
      
      await next()
      
      checkLogFile();
      fileLogger.log("[Request]", datetime, method, path, c.res.status, ip, userAgent)

      if (isDebug) { 
          console.log("[Request]", datetime, method, path, c.res.status, ip, userAgent)
      }        
    }
  }
top

posted at

2024. 7. 13. 04:49


POST : Backend study

Hono by BunJS study

Hono JS라는 걸 발견했는데 이게 꽤 쓸만해 보인다. 간결하고 있을 건 다 있다는 느낌이다.  https://hono.dev/ 여기서 관리하고 있고 https://thevalleyofcode.com/hono 여기에서 스펙을 좀 더 쉽게 확인할 수 있다.  middle ware, basic auth, compress, firebase auth 까지 지원을하고 logger,  ssl도 자체 지원한다. Hono는 Bun or Deno와 사용하면 최고의 성능을 내는 것 같다.  Nest js에 만족하고 있고, 극강의 퍼포먼스가 필요한 것은 아니라서 일단 관심만 두고 있다. Hono는 Node js도 지원하기 때문에 사이드 프로젝트가 있다면 흥미가 당긴다. 

https://medium.com/deno-the-complete-reference/performance-of-hono-framework-with-deno-170ea929c49e 
https://medium.com/deno-the-complete-reference/performance-of-hono-framework-with-bun-b6e592cc113a  
https://github.com/SaltyAom/bun-http-framework-benchmark 
https://hono.dev/docs/concepts/motivation 
https://github.com/honojs/hono  

Macmini m1에서 테스트 했고,  기본적인 가이드는 공식 사이트와 https://thevalleyofcode.com/hono 에서 확인 가능한데, 나는 내가 궁금한 것 위주로 테스트 했다. 

 

Hono 프로젝트 생성

bun , nodejs, deno, 등 여러 플랫폼에서 실행할 수 있는 기본 프로젝트를 생성해준다. 

dajkim76@Kims-Mac-mini ~/test % bun create hono hono_test
create-hono version 0.10.0
✔ Using target directory … hono_test
? Which template do you want to use? bun
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? bun
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd hono_test
dajkim76@Kims-Mac-mini ~/test % cd hono_test
dajkim76@Kims-Mac-mini ~/test/hono_test % cat package.json
{
  "name": "hono_test",
  "scripts": {
    "dev": "bun run --hot src/index.ts"
  },
  "dependencies": {
    "hono": "^4.4.13"
  },
  "devDependencies": {
    "@types/bun": "latest"
  }
}%
dajkim76@Kims-Mac-mini ~/test/hono_test % bun dev
$ bun run --hot src/index.ts
Started server http://localhost:3000

bun으로 돌아가는 프로젝트로 생성된 기본 코드는

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

export default app

 

https (TLS) 지원

export default app을 아래로 바꾸면 tls 지원 끝

export default { 
  port: 3000, 
  fetch: app.fetch, 
  tls: {
    cert: Bun.file("/Users/dajkim76/test/server.crt"),
    key: Bun.file("/Users/dajkim76/test/server.key"),
  },
}

 

compression (gzip 지원)

hono/compress가 bun에서는 아직 미지원이다. 
(https://hono.dev/docs/middleware/builtin/compress 
https://github.com/oven-sh/bun/issues/1723  )

https://www.npmjs.com/package/bun-compression 를 이용해서 gzip을 지원할 수 있을 듯..

 bun add bun-compression

import { compress } from 'bun-compression'

const app = new Hono()

app.use("*", compress())

Postman으로 응답 해서 Content-Encoding gzip 확인

근데 테스트 결과.. bun-compression 미들웨어를 사용하면 httpCode 응답이 200으로 고정되는 문제가 있다.  404 not found 요청도 마찬가지.  c.status(500) 이런게 클라이언트까지 전달되지 않았다. 이유는 모름.

 

https://github.com/oven-sh/bun/issues/1723#issuecomment-1774174194  이쪽 코드를 참고하여 해결했다. 

import { compress } from 'hono/compress' 를 그대로 사용하면서,  미 구현된 CompressionStream과 DecompressionStream 을 핵으로 구현한다. 

import { compress } from 'hono/compress'





// @bun

/*! MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
import zlib from 'node:zlib'

// fyi, Byte streams aren't really implemented anywhere yet
// It only exist as a issue: https://github.com/WICG/compression/issues/31

const make = (ctx, handle) => Object.assign(ctx, {
  writable: new WritableStream({
    write: chunk => handle.write(chunk),
    close: () => handle.end()
  }),
  readable: new ReadableStream({
    type: 'bytes',
    start (ctrl) {
      handle.on('data', chunk => ctrl.enqueue(chunk))
      handle.once('end', () => ctrl.close())
    }
  })
})

globalThis.CompressionStream ??= class CompressionStream {
  constructor(format) {
    make(this, format === 'deflate' ? zlib.createDeflate() :
    format === 'gzip' ? zlib.createGzip() : zlib.createDeflateRaw())
  }
}

globalThis.DecompressionStream ??= class DecompressionStream {
  constructor(format) {
    make(this, format === 'deflate' ? zlib.createInflate() :
    format === 'gzip' ? zlib.createGunzip() :
    zlib.createInflateRaw())
  }
}






const app = new Hono()
app.use(compress())

httpCode도 정상 동작하고 헤더에 gzip도 보인다. 요청시 gzip 인코딩도 될 것 같은데 테스트는 안 해봄.

 

Get Remote IP from HonoJS with Bun

의외로 이게 JS 런타임마다 일관적이지 않다.  뭐 당연하겠지만 그래서 프레임웍마다 구현 가이드가 다 다르고 친절하지도 않다. 

https://bun.sh/blog/bun-v1.0.4#implement-server-requestip  여기를 참고하면 requestIP라는 함수가 제공되는 것을 알 수 있고, Bun 런타임에서의 request 객체를 파라미터로 받는다. 따라서  Hono 어딘가에 requestIP 함수가 있을 것이고 어딘가에 rawRequest 같은게 있어야 한다. 

app.get('/', (c) => {
  console.log(c);
  console.log(c.req);

로 두 가지를 찾아보면 

Context {
  env: DebugHTTPSServer {
    address: {
      address: "::",
      family: "IPv6",
      port: 3000,
    },
    development: true,
    fetch: [Function: fetch],
    hostname: "localhost",
    id: "[http]-tcp:localhost:3000",
    pendingRequests: 1,
    pendingWebSockets: 0,
    port: 3000,
    protocol: "https",
    publish: [Function: publish],
    ref: [Function: ref],
    reload: [Function: reload],
    requestIP: [Function: requestIP], <<<------------------------------- requestIP 함수
    
    
    
HonoRequest {
  raw: Request (0 KB) {  <<<-------------------------------- rawRequest 객체
    method: "GET",

 

따라서 아래처럼 호출해보면

app.get('/', (c) => {
  //console.log(c.req);
  const ip = c.env.requestIP(c.req.raw);  
  console.log(ip);
  console.log(ip.address);
  return c.text('Hello Hono! :' + ip.address)
})


이렇게 응답함..

{
  address: "::ffff:192.168.0.191",
  family: "IPv6",
  port: 57296,
}
::ffff:192.168.0.191

 

Logger

기본 logger, 별로 기능 없는 

import { logger } from "hono/logger"

const app = new Hono()

app.use("*", logger())

실행 화면

  <-- GET /hello
  --> GET /hello 200 2ms
  <-- GET /hello
  --> GET /hello 200 2ms

https://github.com/honojs/hono/blob/main/src/middleware/logger/index.ts logger의 실제 구현부가 매우 간단하고. 날짜, IP, user-Agent 표시도 없고 당연히 file로 쓰는 옵션도 없다.  코드를 참고해서 직접 구현해야 한다..

custom-logger.ts

import { MiddlewareHandler } from "hono/types"

export const customLogger = (): MiddlewareHandler => {
    return async function logger(c, next) {
      const { method, path } = c.req
      const userAgent = c.req.header('user-agent')
      const ip = c.env.requestIP(c.req.raw).address      
      const datetime = new Date().toLocaleString()
      
      await next()        
      console.log(datetime, method, path, c.res.status, ip, userAgent)
    }
  }

 

index.ts

import { customLogger } from './custom-logger';
app.use(customLogger()

 

동작확인

7/13/2024, 12:28:14 AM GET / 200 ::1 PostmanRuntime/7.37.3
7/13/2024, 12:28:16 AM GET / 200 ::1 PostmanRuntime/7.37.3
7/13/2024, 12:28:16 AM GET / 200 ::1 PostmanRuntime/7.37.3
7/13/2024, 12:28:20 AM GET / 200 ::ffff:192.168.0.191 Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36

파일로 쓰기 구현은 설계부터 하자. 당분간 리다이렉트로 땜빵..  

 

기본 인증 basicAuth

import { basicAuth } from 'hono/basic-auth'

const app = new Hono()

app.use('/admin/*', basicAuth({ 
  username: 'test1', 
  password: 'test1'
}));


app.get('/admin', (c) => { 
  //console.log(c.req);
  return c.text('Logged in') 
});

잘 동작 함

 

Rate limit

https://github.com/rhinobase/hono-rate-limiter  

bun add hono-rate-limiter 로 6초에 10번의 요청시 에러 응답

import { rateLimiter } from "hono-rate-limiter";

const app = new Hono()

const limiter = rateLimiter({
  windowMs: 6 * 1000, 
  limit: 10,
  standardHeaders: "draft-6",
  keyGenerator: (c) => { 
    const ip = c.env.requestIP(c.req.raw).address;
    console.log("reqIP:"+ip);
    return ip;
  }, // Method to generate custom identifiers for clients.
});

app.use(limiter);

 

테스트 결과 Limit에 도달시  응답 body에 text로 나간다. 

Too many requests, please try again later.

http 코드는 429가 내려온다. 하지만 타이밍이 좀 이상하다. 응답을 서버가 지연시키고 있는 듯한 느낌.  https://github.com/rhinobase/hono-rate-limiter/blob/main/packages/core/src/core.ts. 코드를 보니  options.message 와 options.handler로 적당히 커스터마이징도 가능할 듯 하다. 

  message: async (c) => {
    return {code: 429, message: "too many requests"};
  }, 
  
  이렇게 하거나 handler를 직접 넣어주면 휠씬 다양하게 대응 가능
  
  handler: async (c, _, options) => {
    // TODO: 이겨서 ip를 저장해 두었다가 1시간 동안 request 블럭시키는 것도 가능할 듯.
    
    // 로그가 안 찍혀서 직접 찍는다. 근데 logger에서 왜 안 찍히지.
    const { method, path } = c.req
    const userAgent = c.req.header('user-agent')
    const ip = c.env.requestIP(c.req.raw).address      
    const datetime = new Date().toLocaleString()    
    console.log(datetime, method, path, options.statusCode, ip, userAgent)

    // Json으로 응답하자
    return c.json({code: 429, message: "block too many requests"}, options.statusCode)
  }

 

Error Handling

https://hono.dev/docs/api/exception 

// 나만의 에러 정의..
class ErrorCodeException extends Error {
  readonly code: number;
  
  constructor(code: number, message: string) {
    super(message)
    this.code = code;
  }
}


app.onError((err, c) => {
  if (err instanceof ErrorCodeException) {
    console.log(`ErrorCodeException: ${err.code} ${err.message}`); 
    return c.json({code: err.code, message: err.message}, 500);
  }
  if (err instanceof HTTPException) {
    // Get the custom response
    console.log(`HTTPException: ${err.message}`); 
    return c.json({code: err.status, message: err.message}, err.status);
  }
  //...
})


어딘가에서 에러를 던지면.. 
throw new ErrorCodeException(1234, `error remote ip: ${ip}`);

HttpCode 500에 json body로 응답한다. 
{
    "code": 1234,
    "message": "error remote ip ::1"
}

HTTPException과 ErrorCodeException과 UnhandledException (이건 nodejs와 동일 할 듯) 처리하고 로그 남길 수 있다면  큰 문제 없을 듯. 중대한 오류일 때는 반드시 파일로 남기거나 Discord로 알리는 코드는 nestjs와 다르지 않을 듯..

 

Firebase IdToken Auth

https://github.com/honojs/middleware/tree/main/packages/firebase-auth   

테스트 안 해봄. 되겠지 뭐.. 

 

Cron

가벼운 프레임웍이라서 cron job 실행은 지원하지 않는다.  별도의 process로 구현하던지 Linux  cron을 이용하던지.. 

 

소감

전체적 느낌적으로 Elysia보다 나은 것 같다.  node, deno에서도 동작하고 여러 플랫폼에서 다양하게 지원되는 가벼운 프레임웍이면서 커스터 마이징도 쉽다. bun과 사용되면 성능도 상위권이다. ( node를 사용한다면  NestJS with fastify 가 나을 수 있다. Elysia도 bun에서는 상위권 성능이지만 Hono와는 대동소이하다.)  https://github.com/honojs/hono 에서 확인해보니 Github Star와 Fork 카운트가 Hono가 두 배 더 많다.  릴리스도 훨씬 더 많이 했다. 코드 기여자도 더 많다. 따라서 정성적, 정량적 모두에서 Hono에 호감이 더 간다. 

 

top

posted at

2024. 7. 12. 23:31


POST : Backend study

Elysia by BunJS study

https://github.com/elysiajs/elysia

 

GitHub - elysiajs/elysia: Ergonomic Framework for Humans

Ergonomic Framework for Humans. Contribute to elysiajs/elysia development by creating an account on GitHub.

github.com

 

프로젝트 생성

bun create elysia elysia_test
cd elysia_test
bun dev
$ bun run --watch src/index.ts
🦊 Elysia is running at localhost:3000

 

TLS(https) 지원

const app = new Elysia()
  .get("/", () => "Hello Elysia")
  .listen({
      port: 3000,
      tls: {
        key: Bun.file("/Users/dajkim76/test/server.key"),
        cert: Bun.file("/Users/dajkim76/test/server.crt"),
      },
      hostname: "0.0.0.0"
    });

 

compression(gzip)지원

bun add elysia-compress
import { compression } from 'elysia-compress'

const app = new Elysia()
  .use(compression({ threshold: 100, encodings: ['deflate', 'gzip']}))
  .get("/hello", () => "Hello Elysia Hello Elysia Hello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello ElysiaHello Elysia")

 

logger 여러 종류가 있지만 
https://github.com/PunGrumpy/logixlysia 간단하고 충분한 스펙이다. 

bun add logixlysia
import logixlysia from 'logixlysia'

const logger = logixlysia({
  config: {
    ip: true,
    logFilePath: './example.log',
    customLogFormat:
      '🦊 {now} {level} {duration} {method} {pathname} {status} {message} {ip}',
    // logFilter: {
    //   level: ['ERROR', 'WARNING'],
    //   status: [500, 404],
    //   method: 'GET'
    // }
  }
})

const app = new Elysia()
  .use(logger)

결과는..

# 콘솔로그
      ┌────────────────────────────────────────────────┐
      │                                                │
      │                 Elysia v1.0.27                 │
      │                                                │
      │  🦊 Elysia is running at https://0.0.0.0:3000  │
      │                                                │
      └────────────────────────────────────────────────┘

🦊 Elysia is running at 0.0.0.0:3000
🦊 7/7/2024, 12:13:38 AM INFO    30ms   GET     /hello 200
🦊 7/7/2024, 12:13:46 AM INFO    1ms    GET     /hello 200
🦊 7/7/2024, 12:13:47 AM INFO    1ms    GET     /hello 200
🦊 7/7/2024, 12:13:47 AM INFO    844µs  GET     /hello 200
🦊 7/7/2024, 12:13:48 AM INFO    1ms    GET

# 파일 로그
dajkim76@Kims-Mac-mini ~/test/elysia_test % cat example.log
🦊 7/7/2024, 12:13:38 AM INFO        39ms         GET     /hello 200
🦊 7/7/2024, 12:13:46 AM INFO         3ms         GET     /hello 200
🦊 7/7/2024, 12:13:47 AM INFO         3ms         GET     /hello 200
🦊 7/7/2024, 12:13:47 AM INFO         2ms         GET     /hello 200
🦊 7/7/2024, 12:13:48 AM INFO         2ms         GET     /hello 200

 

logger 두번째 winston를 지원하는 elysia-logging 이게 나아보인다.

bun add @otherguy/elysia-logging
bun add winston
import { ElysiaLogging } from "@otherguy/elysia-logging";
import { type Logger, LogFormat } from "@otherguy/elysia-logging";
import { createLogger, transports, format } from "winston";

// Define Winston logger
const logger : Logger = createLogger({
  // Use the LOG_LEVEL environment variable, or default to "info"
  level: Bun.env.LOG_LEVEL ?? "info",

  // Use JSON format
  format: format.json(),

  // Log to the console
  transports: [new transports.Console()],
});

const elysiaLogging = ElysiaLogging(logger, {
  level: "info",

  // Access logs in JSON format
  format: LogFormat.JSON,
})

const app = new Elysia()
  .use(elysiaLogging)

결과는

🦊 Elysia is running at 0.0.0.0:3000
{"level":"info","message":"GET /hello completed with status 200 in 20.76ms","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":20761917}}
{"level":"info","message":"GET /hello completed with status 200 in 2.129ms","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":2129250}}
{"level":"info","message":"GET /hello completed with status 200 in 618.7µs","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":618709}}

 

파일로 쓰는 걸로 바꿔보자..

bun add winston-daily-rotate-file

코드 수정

import { ElysiaLogging } from "@otherguy/elysia-logging";
import { type Logger, LogFormat } from "@otherguy/elysia-logging";
import  * as winston from "winston";
const winstonDaily = require('winston-daily-rotate-file')

const logDir = './logs'; // log 파일을 관리할 폴더
const IS_DEVELOPMENT = true;

const dailyOptions = (level: string) => {
  return {
    level,
    datePattern: 'YYYY-MM-DD',
    dirname: logDir + `/${level}`,
    filename: `%DATE%.${level}.log`,
    maxFiles: 7, //7일치 로그파일 저장
    zippedArchive: true, // 로그가 쌓이면 압축하여 관리
    format: winston.format.combine(
      winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 
    ),
  };
};

const consoleOptions = {
  level: 'debug', //console은 debug 이상은 다 출력한다
  format: winston.format.combine(
    //winston.format.colorize({ all: true }),
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  ),
};

const createTransports = () => {
  const transprots : winston.transport[]= [];
  if (IS_DEVELOPMENT) {
    // Console에 출력은 개발모드일때만 한다.
    transprots.push(new winston.transports.Console(consoleOptions));
  }

  transprots.push(new winstonDaily(dailyOptions('debug'))); // 404 not found
  transprots.push(new winstonDaily(dailyOptions('info'))); // normal api request
  transprots.push(new winstonDaily(dailyOptions('warn'))); // error api response
  transprots.push(new winstonDaily(dailyOptions('error'))); // internal error
  return transprots;
};




// Define Winston logger
const logger : Logger = winston.createLogger({
  // Use the LOG_LEVEL environment variable, or default to "info"
  level: Bun.env.LOG_LEVEL ?? "info",

  // Use JSON format
  format: winston.format.json(),

  // Log to the console
  transports: createTransports(),
});

const elysiaLogging = ElysiaLogging(logger, {
  level: "info",

  // Access logs in JSON format
  format: LogFormat.JSON,
})

const app = new Elysia()
  .use(elysiaLogging)

결과는

콘솔은

🦊 Elysia is running at 0.0.0.0:3000
{"level":"info","message":"GET /hello completed with status 200 in 17.24ms","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":17236792}}
{"level":"info","message":"GET /hello completed with status 200 in 515.6µs","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":515583}}
{"level":"info","message":"GET /hello completed with status 200 in 408.4µs","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":408417}}

파일은
dajkim76@Kims-Mac-mini ~/test/elysia_test/logs/info % cat 2024-07-07.info.log
{"level":"info","message":"GET /hello completed with status 200 in 17.24ms","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":17236792}}
{"level":"info","message":"GET /hello completed with status 200 in 515.6µs","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":515583}}
{"level":"info","message":"GET /hello completed with status 200 in 408.4µs","request":{"ip":"127.0.0.1","method":"GET","url":{"params":{},"path":"/hello"}},"response":{"status_code":200,"time":408417}}

NestJS에서 쓰든 코드를 가져왔는데 일단 동작은 한다. 파일로 쓰기도 잘 되고,  logger.error("")로 했을 때 error 디렉토리에도 error만 분리되서 잘 저장된다. 하지만 timestamp가 저장되지 않고 user-Agent로 찍혔으면 좋겠는데.  생각해보니 NestJS에서도 middleware로 HTTP log를 쓰도록 했었네.  구현부를 더 들여다보면 가능할 지도 모르지만 로그는 여기까지만 테스트 ..

Basic Auth

https://github.com/itsyoboieltr/elysia-basic-auth  
https://github.com/eelkevdbos/elysia-basic-auth 

잘 안 됨

 

RateLimit

bun add elysia-rate-limit 

기본값이 6초 내에 10번 이상 요청이 들어오면 429 에러 응답.

import { rateLimit } from 'elysia-rate-limit'


const app = new Elysia()
  .use(rateLimit())

 

cron job

https://elysiajs.com/plugins/cron.html 

 

JWT 인증

https://elysiajs.com/plugins/jwt.html

 

Firebase IdToken 인증

 

유용한 링크

https://mirzaleka.medium.com/bun-crud-api-with-elysia-js-mongodb-10e73d484723

 

 

top

posted at

2024. 7. 7. 00:32


POST : Backend study

firebase functions/v2 마이그레이션

오래전에 구글 클라우드에서 매일이 2개 왔는데  firebase functions 를 v2로 업그레이드 해 달라는 것이고, node 버전을 올려달라는 것이다. 귀찮아서 미루다가 마감이 임박하여 정리한다. 

v2 업데이트는 https://firebase.google.com/docs/functions/2nd-gen-upgrade?hl=ko
index.js 를 참고하면 되고,  
node rumtime 업데이트는 https://cloud.google.com/functions/docs/runtime-support?hl=ko 를 참고한다. function 1개를 쓰고 있는데, 만든지 너무 오래되었다. 오랫동안 아무런 문제없이 잘 실행되었다는게 놀랍다.  오래되서 어떻게 개발하고 배포했는지 잘 기억나지 않는다.  이 문서는 3년 뒤에 다시 할 때 참고하기위한 문서일뿐..  그때되면 다시 https://firebase.google.com/docs/functions/get-started?hl=ko&gen=2nd 여기부터 될 듯.. 

2020년 11월에 배포된게 마지막이고, 하루에 600번 정도 호출되고, Nodejs 12기반이다. 

NODE 런타임 업그레이드

node 12는 2025년 1월 30에 사용이 중단된다.  node 22는 2027년 10월까지 사용할 수 있으므로 22로 업그레이드 한다.  

functions/package.json 에서 22로 변경

 "engines": {
    "node": "22"
  },

테스트를 위해서 

# 노드 설치
nvm install 22

# 노드 사용
nvm use 22

# node를 바꿨으므 CLI 다시 설치
npm install -g firebase-tools

 

node 22는 경고가 있다. 하지만 punycode는 사용하지 않으므로 무시한다. 

(node:95266) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)

 

functions/v2 마이그레이션

# 기존 소스에 있던건데 참조하지 않기에 불필요한 package 제거
npm remove firebase-admin

# functions는 문서에는 4.3.0으로 되어있지만 최신으로 설치
npm install --save firebase-functions@latest

index.js 를 가이드게 맞게 수정한다. 

 

로컬 테스트

https://firebase.google.com/docs/functions/local-emulator?hl=ko
에뮬레이터 : firebase emulators:start (오류가 있다면 firebase init functions 를  다시하되 package.json과 index.js 는 덮어쓰지 말 것. 프로젝트가 너무 오래됬나보다. ) 실행하면 , 주소와 포트가 포함된 URL이 보인다. 에뮬레이터를 잘 만들었다. 

 

리얼 배포

firebase deploy --only functions  

firebase deploy --only functions
...
...
Error: [checkRefund(us-central1)] Upgrading from GCFv1 to GCFv2 is not yet supported. Please delete your old function or wait for this feature to be ready

이런 에러가 뜨는데 GCFv1이 Google Cloud Functions v1의 약자인 것 같다. 즉 v1에서 v2로 자연스러운 업그레이드가 불가능하다는 뜻이다. 따라서 먼저 기존 function을 지우고 다시 배포하면 된다. 그 동안 물론 서비스가 안된다. 흔히 말하는 무중단 배포가 안 된다는 뜻이다. 하지만 제공하는 기능이 무중단배포가 필수가 아니므로 삭제후 배포했다. 잠깐 쫄리기는 했다. 

 

리얼 테스트

에뮬레이터로 테스트 하기는 했지만 리얼에서 한 번 더 한다.  로그 보기로 두 가지를 확인할 수 있다. Remote IP로 내 IP에서 호출된 것임을 확인하고 http 코드 200으로 정상 동작을 확인한다. 

 

 

 

top

posted at

2024. 7. 3. 23:45


POST : Backend study

우분투 vm 생성후 각종 서버 설정

https://www.youtube.com/watch?v=4yXIYUzsjbA&t=325s 를 참고했다. 

======= new 계정 생성 (daejeong) =======
# ID 추가
sudo adduser daejeong

		dajkim76@base-backend-1:~$ sudo adduser daejeong
		Adding user `daejeong' ...
		Adding new group `daejeong' (1004) ...
		Adding new user `daejeong' (1003) with group `daejeong' ...
		Creating home directory `/home/daejeong' ...
		Copying files from `/etc/skel' ...
		New password:
		Retype new password:
		No password supplied
		New password:
		Retype new password:
		passwd: password updated successfully
		Changing the user information for daejeong
		Enter the new value, or press ENTER for the default
			Full Name []:
			Room Number []:
			Work Phone []:
			Home Phone []:
			Other []:
		Is the information correct? [Y/n]

#sudo 권한 주기
sudo usermod -aG sudo daejeong


# 로그인 후 ssh 설정 복사하기
sudo su - daejeong
sudo cp -r /home/dajkim76/.ssh /home/daejeong/
sudo chown -R daejeong:daejeong /home/daejeong/.ssh

======== time zone ===========
# 타임존 확인
timedatectl

		daejeong@base-backend-1:~$ timedatectl
		               Local time: Fri 2024-06-28 17:12:14 UTC
		           Universal time: Fri 2024-06-28 17:12:14 UTC
		                 RTC time: Fri 2024-06-28 17:12:14
		                Time zone: Etc/UTC (UTC, +0000)
		System clock synchronized: yes
		              NTP service: active
		          RTC in local TZ: no


# Aisa/Seoul로 변경
sudo timedatectl set-timezone Asia/Seoul		          


========= docker 설치 =========
# apt 업데이트
sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

		// 에러 발생시
		sudo killall apt apt-get
		// lock 삭제
		sudo rm /var/lib/apt/lists/lock
		sudo rm /var/cache/apt/archives/lock
		sudo rm /var/lib/dpkg/lock*
		// 마무리
		sudo rm -rf /var/lib/dpkg/updates/*
		sudo dpkg --configure -a
		sudo apt update

# 도커 GPG 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add

# repository 경로 추가
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

# 도커 설치
sudo apt update
sudo apt install docker-ce

#도커 소켓 권한
sudo chmod 666 /var/run/docker.sock
sudo chown root:docker /var/run/docker.sock

# docker를 sudo없이 사용하기위해서
sudo usermod -aG docker daejeong
newgrp docker

# 도커 설치 확인
docker info
service docker status
		● docker.service - Docker Application Container Engine
		     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
		     Active: active (running) since Sat 2024-06-29 02:34:11 KST; 19min ago
		TriggeredBy: ● docker.socket
		       Docs: https://docs.docker.com
		   Main PID: 20815 (dockerd)
		      Tasks: 11
		     Memory: 33.2M
		     CGroup: /system.slice/docker.service
		             └─20815 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock


# docker compose 설치
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose
docker-compose version

# hello world 실행해보기
docker run hello-world


========= nvm & node ==========
# nvm 설치 'https://github.com/nvm-sh/nvm/releases'
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

source ~/.bashrc

# 사용가능한 node 조회
nvm ls-remote

# node 20.x 최신설치
nvm install 20

		daejeong@base-backend-1:~$ node -v
		v20.15.0
		daejeong@base-backend-1:~$ npm -v
		10.7.0

		nvm use 20.15.0
		nvm unsintall 20.15.0
		nvm use 20.15.0

# 443 포트에서 node실행하려면 권한추가 
which node
	/home/daejeong/.nvm/versions/node/v20.15.0/bin/node

sudo setcap 'cap_net_bind_service=+ep' /home/daejeong/.nvm/versions/node/v20.15.0/bin/node


========= bun js runtime 설치 ========
curl -fsSL https://bun.sh/install | bash

source ~/.bashrc

bun --version
		daejeong@base-backend-1:~$ bun --version
		1.1.17

		나중에 업그레이드
		bun upgrade


======= 기타 nginx, git =========
sudo apt install nginx

cat /etc/nginx/sites-enabled/default

sudo service nginx start


sudo apt install git


======== 보안상 안려진 계정 삭제 ========
sudo deluser ubuntu



====== 주기적 실행 ========
daejeong@base-backend-1:~$ cat cron_job.sh
		#!/bin/sh

		echo $(date +'%Y%m%d_%H%M%S') >> ~/cron.log

chmod +x cron_job.sh


# crontab -e는 현재 사용자의 주기적 실행이므로 실행 아이디를 지정할 필요가 없고, 저장하면 자동으로 반영됨 
crontab -e

# 매 분 마다 실행
* * * * * ~/cron_job.sh
		
# 몇 분후 결과 화인
		daejeong@base-backend-1:~$ cat cron.log
		20240629_034101
		20240629_034201
		20240629_034302
		20240629_034401

or

vi /etc/crontab
sudo service cron restart
sudo service cron status
		참고..
		https://okkks.tistory.com/904


======= 서버 자동 실행 =======
sudo vi  /lib/systemd/system/rc-local.service

#맨 마지막에 아래 두 줄 추가
[Install]
WantedBy=multi-user.target

# 설정파일에 rc.local의 경로가 나온다. 
# ConditionFileIsExecutable=/etc/rc.local
# /etc/rc.d/rc.local  아니면 /etc/rc.local 이다. 

sudo vi /etc/rc.local
		#!/bin/bash

		#test
		su - daejeong -c 'echo "reboot at $(date +"%Y%m%d_%H%M%S")"'

# 실행권한 주기
sudo chmod +x /etc/rc.local

# 서비스 등록
sudo systemctl enable rc-local.service

# reboot후에 정상 실행 확인
sudo reboot

# 좀 있다 다시 접속해서 rc-local.service 동작 확인 (reboot at 20240629_034720 확인)
sudo systemctl status rc-local.service

		daejeong@base-backend-1:~$ sudo systemctl status rc-local.service
		[sudo] password for daejeong:
		● rc-local.service - /etc/rc.local Compatibility
		     Loaded: loaded (/lib/systemd/system/rc-local.service; enabled; vendor preset: enabled)
		    Drop-In: /usr/lib/systemd/system/rc-local.service.d
		             └─debian.conf
		     Active: active (exited) since Sat 2024-06-29 03:47:20 KST; 1min 30s ago
		       Docs: man:systemd-rc-local-generator(8)
		      Tasks: 0 (limit: 650)
		     Memory: 0B
		     CGroup: /system.slice/rc-local.service

		Jun 29 03:47:11 base-backend-1 systemd[1]: Starting /etc/rc.local Compatibility...
		Jun 29 03:47:13 base-backend-1 su[499]: (to daejeong) root on none
		Jun 29 03:47:14 base-backend-1 su[499]: pam_unix(su-l:session): session opened for user daejeong by (uid=0)
		Jun 29 03:47:21 base-backend-1 rc.local[605]: reboot at 20240629_034720
		Jun 29 03:47:20 base-backend-1 systemd[1]: Started /etc/rc.local Compatibility.
top

posted at

2024. 6. 29. 04:44


POST : Android Dev Study

이미지를 pdf로 저장하기

참고

https://www.baeldung.com/java-pdf-creation 

itext 를 이용하는 방법

라이센스 문제가 있다. 상업적 이용은 라이센스를 구매하거나, 구매하지 않는다면  내 소스코드를 공개해야한다.  (AGPL)

    implementation 'com.itextpdf:itextg:5.5.10' // images to pdf
	// or     implementation 'com.itextpdf:itextpdf:5.5.13.4' // images to pdf
import com.itextpdf.text.Document
import com.itextpdf.text.Image
import com.itextpdf.text.PageSize
import com.itextpdf.text.pdf.PdfWriter

import java.io.ByteArrayOutputStream
import java.io.FileInputStream

// itextpdf
    fun writeImagesForItext(uriStrList: List<String>, outputUri: Uri): Int {
        var count: Int = 0
        try {
            val document = Document(PageSize.A4, 15f, 15f, 15f, 15f)
            val pdfWriter = PdfWriter.getInstance(document, contentResolver.openOutputStream(outputUri))
            document.open()
            val stream = ByteArrayOutputStream()
            for (uriStr in uriStrList) {
                try {
                    stream.reset() // for reuse
                    val uri = Uri.parse(uriStr)
                    val bitmap: Bitmap = if (uri.scheme != "content") {
                        BitmapFactory.decodeStream(FileInputStream(uriStr))
                    } else {
                        contentResolver.openInputStream(uri).use {
                            BitmapFactory.decodeStream(it)
                        }
                    }

                    var quality = Settings.writePdfImageQuality.value
                    if (quality < 5) quality = 5
                    if (quality > 100) quality = 100
                    bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
                    if (true) {
                        val image = Image.getInstance(stream.toByteArray())
                        document.setPageSize(image)
                        document.newPage()
                        image.setAbsolutePosition(0f, 0f)
                        document.add(image)
                    }
                    bitmap.recycle()
                    count++
                } catch (ex: Exception) {
                    if (BuildConfig.DEBUG) {
                        DUtils.alert(ex.toString())
                    }
                }
            }

            // close
            document.close()
            pdfWriter?.close()
        } catch (ex: Exception) {
            if (BuildConfig.DEBUG) {
                DUtils.alert(ex.toString())
            }
        }
        return count
    }

 

Apache pdfbox를 이용하는 방법

https://pdfbox.apache.org/download.html 를 이용할 수 있지만  Android에서는 바로 사용할 수 없다. Android으로 랩핑한 라이브러리를 이용한다. https://github.com/TomRoush/PdfBox-Android 

implementation 'com.tom-roush:pdfbox-android:2.0.27.0'
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.pdmodel.PDPage
import com.tom_roush.pdfbox.pdmodel.PDPageContentStream
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject



// apache pdfbox
    fun writeImagesForPdfBox(uriStrList: List<String>, outputUri: Uri): Int {
        var count: Int = 0
        try {
            val document = PDDocument()
            val stream = ByteArrayOutputStream()
            for (uriStr in uriStrList) {
                try {
                    stream.reset() // for reuse
                    val uri = Uri.parse(uriStr)
                    val bitmap: Bitmap = if (uri.scheme != "content") {
                        BitmapFactory.decodeStream(FileInputStream(uriStr))
                    } else {
                        contentResolver.openInputStream(uri).use {
                            BitmapFactory.decodeStream(it)
                        }
                    }

                    var quality = Settings.writePdfImageQuality.value
                    if (quality < 5) quality = 5
                    if (quality > 100) quality = 100
                    bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
                    if (true) {
                        val page = PDPage(PDRectangle(bitmap.width.toFloat(), bitmap.height.toFloat()))
                        document.addPage(page)
                        val img = PDImageXObject.createFromByteArray(document, stream.toByteArray(), "test")
                        val contentStream = PDPageContentStream(document, page)
                        contentStream.drawImage(img, 0f, 0f)
                        contentStream.close()
                    }
                    bitmap.recycle()
                    count++
                } catch (ex: Exception) {
                    if (BuildConfig.DEBUG) {
                        DUtils.alert(ex.toString())
                    }
                }
            }

            document.save(contentResolver.openOutputStream(outputUri));
            document.close();
        } catch (ex: Exception) {
            if (BuildConfig.DEBUG) {
                DUtils.alert(ex.toString())
            }
        }
        return count
    }

 

top

posted at

2024. 6. 19. 18:56


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크