paint-brush
FUSE 및 Node.js를 사용하여 동적 파일 시스템을 구축하는 방법: 실용적인 접근 방식~에 의해@rglr
386 판독값
386 판독값

FUSE 및 Node.js를 사용하여 동적 파일 시스템을 구축하는 방법: 실용적인 접근 방식

~에 의해 Aleksandr Zinin33m2024/06/13
Read on Terminal Reader

너무 오래; 읽다

sshfs user@remote:~/ /mnt/remoteroot를 실행하면 어떤 일이 일어나는지 궁금하신가요? 원격 서버의 파일이 어떻게 로컬 시스템에 나타나고 그렇게 빠르게 동기화됩니까? 마치 파일 시스템에 있는 파일인 것처럼 Wikipedia 문서를 편집할 수 있는 WikipediaFS에 대해 들어보셨나요? 이것은 마법이 아닙니다. FUSE(사용자 공간의 파일 시스템)의 힘입니다. FUSE를 사용하면 OS 커널이나 하위 수준 프로그래밍 언어에 대한 깊은 지식이 없어도 자신만의 파일 시스템을 만들 수 있습니다. 이 기사에서는 Node.js 및 TypeScript와 함께 FUSE를 사용하는 실용적인 솔루션을 소개합니다. FUSE가 내부적으로 어떻게 작동하는지 살펴보고 실제 작업을 해결하여 응용 프로그램을 시연해 보겠습니다. FUSE와 Node.js의 세계로 흥미진진한 모험을 떠나보세요.
featured image - FUSE 및 Node.js를 사용하여 동적 파일 시스템을 구축하는 방법: 실용적인 접근 방식
Aleksandr Zinin HackerNoon profile picture

sshfs user@remote:~/ /mnt/remoteroot 실행하면 어떤 일이 일어나는지 궁금하신가요? 원격 서버의 파일이 로컬 시스템에 어떻게 나타나고 그렇게 빠르게 동기화됩니까? 마치 파일 시스템에 있는 파일인 것처럼 Wikipedia 기사를 편집할 수 있는 WikipediaFS 에 대해 들어보신 적이 있습니까? 이것은 마법이 아닙니다. FUSE(사용자 공간의 파일 시스템)의 힘입니다. FUSE를 사용하면 OS 커널이나 하위 수준 프로그래밍 언어에 대한 깊은 지식이 없어도 자신만의 파일 시스템을 만들 수 있습니다.


이 기사에서는 Node.js 및 TypeScript와 함께 FUSE를 사용하는 실용적인 솔루션을 소개합니다. FUSE가 내부적으로 어떻게 작동하는지 살펴보고 실제 작업을 해결하여 응용 프로그램을 시연해 보겠습니다. FUSE와 Node.js의 세계로 흥미진진한 모험을 떠나보세요.

소개

저는 작업 중 미디어 파일(주로 이미지)을 담당했습니다. 여기에는 측면 또는 상단 배너, 채팅 미디어, 스티커 등 많은 것들이 포함됩니다. 물론 "배너는 PNG 또는 WEBP, 300x1000픽셀입니다."와 같은 요구 사항이 많이 있습니다. 요구 사항이 충족되지 않으면 백오피스에서 이미지를 전송하지 않습니다. 그리고 객체 중복 제거 메커니즘이 있습니다. 어떤 이미지도 동일한 강에 두 번 들어갈 수 없습니다.


이로 인해 테스트 목적으로 대량의 이미지 세트가 있는 상황이 발생합니다. 나는 내 삶을 더 쉽게 만들기 위해 쉘 원 라이너 또는 별칭을 사용했습니다.


예를 들어:

 convert -size 300x1000 xc:gray +noise random /tmp/out.png


노이즈 이미지의 예


bashconvert 의 조합은 훌륭한 도구이지만 분명히 이것이 문제를 해결하는 가장 편리한 방법은 아닙니다. QA 팀의 상황을 논의하면 더 복잡한 문제가 드러납니다. 이미지 생성에 소요되는 상당한 시간 외에도 문제 조사 시 첫 번째 질문은 "고유한 이미지를 업로드하셨나요?"입니다. 이것이 얼마나 짜증나는 일인지 당신도 이해하리라 믿습니다.

사용하고 싶은 기술을 선택하자

간단한 접근 방식을 취할 수 있습니다. GET /image/1000x100/random.zip?imagesCount=100 과 같이 자체 설명이 가능한 파일로 경로를 제공하는 웹 서비스를 생성합니다. 경로는 고유한 이미지 세트가 포함된 ZIP 파일을 반환합니다. 듣기에는 좋지만 주요 문제는 해결되지 않습니다. 업로드된 모든 파일은 테스트를 위해 고유해야 합니다.


다음 생각은 "페이로드를 보낼 때 페이로드를 교체할 수 있나요?"일 수 있습니다. QA 팀은 API 호출에 Postman을 사용합니다. Postman 내부를 조사한 결과 요청 본문을 "즉시" 변경할 수 없다는 것을 깨달았습니다.


또 다른 해결책은 파일 읽기를 시도할 때마다 파일 시스템의 파일을 바꾸는 것입니다. Linux에는 디렉터리 변경이나 파일 수정과 같은 파일 시스템 이벤트에 대해 경고하는 Inotify라는 알림 하위 시스템이 있습니다. "Visual Studio Code가 이 큰 작업 영역에서 파일 변경 사항을 감시할 수 없습니다."라는 메시지가 표시되면 Inotify에 문제가 있는 것입니다. 디렉터리가 변경되거나, 파일 이름이 바뀌거나, 파일이 열리거나 하는 등의 경우 이벤트를 발생시킬 수 있습니다.


전체 이벤트 목록은 여기에서 확인할 수 있습니다: https://sites.uclouvain.be/SystInfo/usr/include/linux/inotify.h.html


따라서 계획은 다음과 같습니다.

  1. IN_OPEN 이벤트를 수신하고 파일 설명자를 계산합니다.

  2. IN_CLOSE 이벤트를 수신합니다. 개수가 0으로 떨어지면 파일을 교체합니다.


듣기에는 좋지만 여기에는 몇 가지 문제가 있습니다.

  • Linux만 inotify 지원합니다.
  • 파일에 대한 병렬 요청은 동일한 데이터를 반환해야 합니다.
  • 파일에 집중적인 IO 작업이 있는 경우 교체가 발생하지 않습니다.
  • Inotify 이벤트를 제공하는 서비스가 충돌하는 경우 파일은 사용자 파일 시스템에 유지됩니다.


이러한 문제를 해결하기 위해 자체 파일 시스템을 작성할 수 있습니다. 그러나 또 다른 문제가 있습니다. 일반 파일 시스템은 OS 커널 공간에서 실행됩니다. 이를 위해서는 OS 커널에 대해 알아야 하고 C/Rust와 같은 언어를 사용해야 합니다. 또한 각 커널에 대해 특정 모듈(드라이버)을 작성해야 합니다.


따라서 파일 시스템을 작성하는 것은 우리가 해결하려는 문제에 비해 과잉입니다. 앞으로 긴 주말이 있더라도. 다행히도 이 괴물을 길들일 수 있는 방법이 있습니다. 바로 FUSE (rspace 사용 ) 파일 시스템입니다. FUSE는 커널 코드를 편집하지 않고도 파일 시스템을 만들 수 있는 프로젝트입니다. 이는 복잡한 코어 관련 로직 없이 FUSE를 통한 모든 프로그램이나 스크립트가 플래시, 하드 드라이브 또는 SSD를 에뮬레이트할 수 있음을 의미합니다.


즉, 일반 사용자 공간 프로세스는 자신의 파일 시스템을 생성할 수 있으며, 이는 Nautilus, Dolphin, ls 등 원하는 일반 프로그램을 통해 정상적으로 액세스할 수 있습니다.


FUSE가 요구 사항을 충족하는 데 왜 좋은가요? FUSE 기반 파일 시스템은 사용자 공간 프로세스를 기반으로 구축됩니다. 따라서 libfuse 에 대한 바인딩이 있는 알고 있는 모든 언어를 사용할 수 있습니다. 또한 FUSE를 사용하면 크로스 플랫폼 솔루션을 얻을 수 있습니다.


저는 NodeJS와 TypeScript에 대해 많은 경험을 갖고 있으며, 새로운 FS의 실행 환경으로 이 (훌륭한) 조합을 선택하고 싶습니다. 게다가 TypeScript는 탁월한 객체 지향 기반을 제공합니다. 이를 통해 공개 GitHub 저장소에서 찾을 수 있는 소스 코드뿐만 아니라 프로젝트의 구조도 보여줄 수 있습니다.

FUSE에 대해 자세히 알아보기

공식 FUSE 페이지 에서 인용문을 제공하겠습니다.

FUSE는 사용자 공간 파일 시스템 프레임워크입니다. 커널 모듈(fuse.ko), 사용자 공간 라이브러리(libfuse.*), 마운트 유틸리티(fusermount)로 구성됩니다.


파일 시스템 작성을 위한 프레임워크는 흥미로울 것 같습니다.


각 FUSE 부분이 무엇을 의미하는지 설명해야 합니다.

  1. fuse.ko 는 커널과 관련된 모든 하위 수준 작업을 수행합니다. 이를 통해 OS 커널에 대한 개입을 피할 수 있습니다.


  2. libfuse fuse.ko 와의 통신을 위한 상위 계층을 제공하는 라이브러리입니다.


  3. fusermount 사용자 공간 파일 시스템을 마운트/마운트 해제할 수 있습니다(나를 Captain Obvious라고 불러주세요!).


일반적인 원칙은 다음과 같습니다.
FUSE의 일반 원칙


사용자 공간 프로세스(이 경우 ls )는 요청을 FUSE 커널 모듈로 라우팅하는 가상 파일 시스템 커널에 요청합니다. 그러면 FUSE 모듈은 요청을 사용자 공간으로 다시 라우팅하여 파일 시스템 구현(위 그림의 ./hello )으로 보냅니다.


가상 파일 시스템 이름에 속지 마십시오. FUSE와 직접적인 관련은 없습니다. 사용자 공간 프로그램에 파일 시스템 인터페이스를 제공하는 것은 커널의 소프트웨어 계층입니다. 단순화를 위해 복합 패턴 으로 인식할 수 있습니다.


libfuse 상위 수준과 하위 수준의 두 가지 유형의 API를 제공합니다. 그것들은 유사점이지만 결정적인 차이점을 가지고 있습니다. 낮은 수준의 것은 비동기식이며 inodes 에서만 작동합니다. 이 경우 비동기식은 하위 수준 API를 사용하는 클라이언트가 응답 메서드를 자체적으로 호출해야 함을 의미합니다.


상위 레벨은 보다 "추상적인" inodes 대신 편리한 경로(예: /etc/shadow )를 사용하고 동기화 방식으로 응답을 반환하는 기능을 제공합니다. 이 기사에서는 하위 수준 및 inodes 가 아닌 상위 수준이 어떻게 작동하는지 설명합니다.


자신만의 파일 시스템을 구현하려면 VFS에서 제공하는 요청을 담당하는 일련의 메서드를 구현해야 합니다. 가장 일반적인 방법은 다음과 같습니다.


  • open(path, accessFlags): fd -- 경로로 파일을 엽니다. 이 메소드는 소위 파일 설명자(이하 fd )라고 불리는 숫자 식별자를 반환해야 합니다. 액세스 플래그는 클라이언트 프로그램이 수행하려는 작업(읽기 전용, 쓰기 전용, 읽기-쓰기, 실행 또는 검색)을 설명하는 이진 마스크입니다.


  • read(path, fd, Buffer, size, offset): count of bytes read - 전달된 버퍼에 fd 파일 설명자와 연결된 파일에서 size 바이트를 읽습니다. fd를 사용할 것이므로 path 인수는 무시됩니다.


  • write(path, fd, Buffer, size, offset): count of bytes written - Buffer의 size 바이트를 fd 와 연결된 파일에 씁니다.


  • release(fd) - fd 닫습니다.


  • truncate(path, size) -- 파일 크기를 변경합니다. 파일을 다시 작성하려면 메서드를 정의해야 합니다.


  • getattr(path) -- 크기, 생성 위치, 액세스 위치 등과 같은 파일 매개변수를 반환합니다. 이 메소드는 파일 시스템에서 가장 호출하기 쉬운 메소드이므로 최적의 메소드를 생성해야 합니다.


  • readdir(path) -- 모든 하위 디렉터리를 읽습니다.


위의 방법은 높은 수준의 FUSE API를 기반으로 구축되어 완벽하게 작동 가능한 각 파일 시스템에 필수적입니다. 그러나 목록은 완전하지 않습니다. 전체 목록은 https://libfuse.github.io/doxygen/structfuse__Operations.html 에서 찾을 수 있습니다.


파일 설명자의 개념을 다시 살펴보려면: MacOS를 포함한 UNIX 계열 시스템에서 파일 설명자는 파일과 소켓 및 파이프와 같은 기타 I/O 리소스에 대한 추상화입니다. 프로그램이 파일을 열면 OS는 파일 설명자라는 숫자 식별자를 반환합니다. 이 정수는 각 프로세스에 대한 OS의 파일 설명자 테이블에서 인덱스 역할을 합니다. FUSE를 사용하여 파일 시스템을 구현할 때 파일 설명자를 직접 생성해야 합니다.


클라이언트가 파일을 열 때의 호출 흐름을 고려해 보겠습니다.

  1. getattr(path: /random.png) → { size: 98 }; 클라이언트가 파일 크기를 얻었습니다.


  2. open(path: /random.png) → 10; 경로별로 파일을 열었습니다. FUSE 구현은 파일 설명자 번호를 반환합니다.


  3. read(path: /random.png, fd: 10 buffer, size: 50, offset: 0) → 50; 처음 50바이트를 읽습니다.


  4. read(path: /random.png, fd: 10 buffer, size: 50, offset: 50) → 48; 다음 50개를 읽습니다. 파일 크기로 인해 48바이트를 읽었습니다.


  5. release(10); 모든 데이터를 읽었으므로 fd에 가깝습니다.

최소 실행 가능 제품을 작성하고 이에 대한 Postman의 반응을 확인해 보겠습니다.

다음 단계는 libfuse 기반으로 최소 파일 시스템을 개발하여 Postman이 사용자 정의 파일 시스템과 어떻게 상호 작용하는지 테스트하는 것입니다.


FS에 대한 허용 요구 사항은 간단합니다. FS의 루트에는 읽을 때마다 해당 콘텐츠가 고유해야 하는 random.txt 파일이 포함되어야 합니다(이를 "항상 고유한 읽기"라고 함). 콘텐츠에는 임의의 UUID와 ISO 형식의 현재 시간이 새 줄로 구분되어 포함되어야 합니다. 예를 들어:

 3790d212-7e47-403a-a695-4d680f21b81c 2012-12-12T04:30:30


최소 제품은 두 부분으로 구성됩니다. 첫 번째는 HTTP POST 요청을 수락하고 요청 본문을 터미널에 인쇄하는 간단한 웹 서비스입니다. 코드는 매우 간단하며 시간을 들일 가치가 없습니다. 주로 기사가 Express가 아닌 FUSE에 관한 것이기 때문입니다. 두 번째 부분은 요구 사항을 충족하는 파일 시스템을 구현하는 것입니다. 코드는 83줄에 불과합니다.


코드의 경우 libfuse 의 고급 API에 대한 바인딩을 제공하는 node-fuse-binds 라이브러리를 사용합니다.


아래 코드를 건너뛸 수 있습니다. 아래에 코드 요약을 작성하겠습니다.

 const crypto = require('crypto'); const fuse = require('node-fuse-bindings'); // MOUNT_PATH is the path where our filesystem will be available. For Windows, this will be a path like 'D://' const MOUNT_PATH = process.env.MOUNT_PATH || './mnt'; function getRandomContent() { const txt = [crypto.randomUUID(), new Date().toISOString(), ''].join('\n'); return Buffer.from(txt); } function main() { // fdCounter is a simple counter that increments each time a file is opened // using this we can get the file content, which is unique for each opening let fdCounter = 0; // fd2ContentMap is a map that stores file content by fd const fd2ContentMap = new Map(); // Postman does not work reliably if we give it a file with size 0 or just the wrong size, // so we precompute the file size // it is guaranteed that the file size will always be the same within one run, so there will be no problems with this const randomTxtSize = getRandomContent().length; // fuse.mount is a function that mounts the filesystem fuse.mount( MOUNT_PATH, { readdir(path, cb) { console.log('readdir(%s)', path); if (path === '/') { return cb(0, ['random.txt']); } return cb(0, []); }, getattr(path, cb) { console.log('getattr(%s)', path); if (path === '/') { return cb(0, { // mtime is the file modification time mtime: new Date(), // atime is the file access time atime: new Date(), // ctime is the metadata or file content change time ctime: new Date(), size: 100, // mode is the file access flags // this is a mask that defines access rights to the file for different types of users // and the type of file itself mode: 16877, // file owners // in our case, it will be the owner of the current process uid: process.getuid(), gid: process.getgid(), }); } if (path === '/random.txt') { return cb(0, { mtime: new Date(), atime: new Date(), ctime: new Date(), size: randomTxtSize, mode: 33188, uid: process.getuid(), gid: process.getgid(), }); } cb(fuse.ENOENT); }, open(path, flags, cb) { console.log('open(%s, %d)', path, flags); if (path !== '/random.txt') return cb(fuse.ENOENT, 0); const fd = fdCounter++; fd2ContentMap.set(fd, getRandomContent()); cb(0, fd); }, read(path, fd, buf, len, pos, cb) { console.log('read(%s, %d, %d, %d)', path, fd, len, pos); const buffer = fd2ContentMap.get(fd); if (!buffer) { return cb(fuse.EBADF); } const slice = buffer.slice(pos, pos + len); slice.copy(buf); return cb(slice.length); }, release(path, fd, cb) { console.log('release(%s, %d)', path, fd); fd2ContentMap.delete(fd); cb(0); }, }, function (err) { if (err) throw err; console.log('filesystem mounted on ' + MOUNT_PATH); }, ); } // Handle the SIGINT signal separately to correctly unmount the filesystem // Without this, the filesystem will not be unmounted and will hang in the system // If for some reason unmount was not called, you can forcibly unmount the filesystem using the command // fusermount -u ./MOUNT_PATH process.on('SIGINT', function () { fuse.unmount(MOUNT_PATH, function () { console.log('filesystem at ' + MOUNT_PATH + ' unmounted'); process.exit(); }); }); main();


파일의 권한 비트에 대한 지식을 새로 고치는 것이 좋습니다. 권한 비트는 파일과 관련된 비트 집합입니다. 이는 파일을 읽고, 쓰고, 실행할 수 있는 사람을 나타내는 이진 표현입니다. "누구"에는 소유자, 소유자 그룹 및 기타의 세 그룹이 포함됩니다.


각 그룹에 대해 개별적으로 권한을 설정할 수 있습니다. 일반적으로 각 권한은 읽기(이진수 체계에서 4 또는 '100'), 쓰기(2 또는 '010'), 실행(1 또는 '001')의 세 자리 숫자로 표시됩니다. 이 숫자를 함께 추가하면 결합된 권한이 생성됩니다. 예를 들어 4 + 2(또는 '100' + '010')는 6('110')이 되며 이는 읽기 + 쓰기(RO) 권한을 의미합니다.


파일 소유자의 액세스 마스크가 7(이진수로 111, 읽기, 쓰기 및 실행을 의미)인 경우 그룹에는 5(101, 읽기 및 실행을 의미)가 있고 다른 그룹에는 4(100, 읽기 전용을 의미)가 있습니다. 따라서 파일의 전체 액세스 마스크는 10진수로 754입니다. 실행 권한은 디렉터리에 대한 읽기 권한이 된다는 점을 명심하세요.


파일 시스템 구현으로 돌아가서 이에 대한 텍스트 버전을 만들어 보겠습니다. ( open 호출을 통해) 파일이 열릴 때마다 정수 카운터가 증가하여 열기 호출에서 반환된 파일 설명자를 생성합니다. 그런 다음 임의의 콘텐츠가 생성되어 파일 설명자를 키로 사용하여 키-값 저장소에 저장됩니다. 읽기 호출이 이루어지면 해당 콘텐츠 부분이 반환됩니다.


릴리스 호출 시 콘텐츠가 삭제됩니다. Ctrl+C를 누른 후 파일 시스템을 마운트 해제하려면 SIGINT 처리해야 합니다. 그렇지 않으면 fusermount -u ./MOUNT_PATH 사용하여 터미널에서 수동으로 수행해야 합니다.


이제 테스트에 들어갑니다. 웹 서버를 실행한 다음, 향후 FS의 루트 폴더로 빈 폴더를 생성하고 기본 스크립트를 실행합니다. "포트 3000에서 수신하는 서버" 줄이 인쇄된 후 Postman을 열고 매개변수를 변경하지 않고 연속적으로 웹 서버에 몇 가지 요청을 보냅니다.
왼쪽은 FS이고 오른쪽은 웹 서버입니다.


모든 것이 좋아 보인다! 각 요청에는 예상한 대로 고유한 파일 콘텐츠가 있습니다. 로그는 또한 위의 "FUSE 심층 분석" 섹션에서 설명한 파일 열기 호출의 흐름이 정확하다는 것을 증명합니다.


MVP가 포함된 GitHub 리포지토리: https://github.com/pinkiesky/node-fuse-mvp . 이 코드를 로컬 환경에서 실행하거나 이 저장소를 자체 파일 시스템 구현을 위한 상용구로 사용할 수 있습니다.

핵심 아이디어

접근 방식이 확인되었습니다. 이제 기본 구현 시간입니다.


"항상 고유한 읽기"를 구현하기 전에 가장 먼저 구현해야 할 것은 원본 파일에 대한 작업을 생성하고 삭제하는 것입니다. 가상 파일 시스템 내의 디렉터리를 통해 이 인터페이스를 구현하겠습니다. 사용자는 "항상 고유하게" 또는 "무작위로" 만들고 싶은 원본 이미지를 넣으면 파일 시스템이 나머지를 준비합니다.


여기와 다음 섹션에서 "항상 고유한 읽기", "무작위 이미지" 또는 "무작위 파일"은 읽을 때마다 고유한 콘텐츠를 이진 의미로 반환하는 파일을 의미하며 시각적으로는 가능한 한 유사하게 유지됩니다. 원본에.


파일 시스템의 루트에는 Image Manager와 Images라는 두 개의 디렉터리가 포함됩니다. 첫 번째는 사용자의 원본 파일을 관리하는 폴더입니다(CRUD 저장소라고 생각하시면 됩니다). 두 번째는 임의의 이미지가 포함된 사용자 관점의 관리되지 않는 디렉터리입니다.
사용자가 파일 시스템과 상호 작용


터미널 출력으로서의 FS 트리


위 이미지에서 볼 수 있듯이 "항상 고유한" 이미지뿐만 아니라 파일 변환기도 구현할 예정입니다! 추가 보너스입니다.


우리 구현의 핵심 아이디어는 프로그램에 공통 FUSE 방법을 제공하는 각 노드와 리프가 있는 객체 트리가 포함된다는 것입니다. 프로그램이 FS 호출을 받으면 해당 경로를 통해 트리에서 노드나 리프를 찾아야 합니다. 예를 들어, 프로그램은 getattr(/Images/1/original/) 호출을 받은 다음 경로가 지정된 노드를 찾으려고 시도합니다.


이 같은: FS 트리 예


다음 질문은 원본 이미지를 어떻게 저장할 것인가입니다. 프로그램의 이미지는 바이너리 데이터와 메타 정보(메타에는 원본 파일 이름, 파일 MIME 유형 등이 포함됨)로 구성됩니다. 바이너리 데이터는 바이너리 저장소에 저장됩니다. 이를 단순화하고 사용자(또는 호스트) 파일 시스템에 바이너리 파일 집합으로 바이너리 저장소를 구축해 보겠습니다. 메타 정보는 사용자 파일 시스템의 텍스트 파일 내에 JSON과 유사하게 저장됩니다.


기억하시겠지만 "최소 실행 가능한 제품을 작성하자" 섹션에서 템플릿을 통해 텍스트 파일을 반환하는 파일 시스템을 만들었습니다. 여기에는 임의의 UUID와 현재 날짜가 포함되어 있으므로 데이터의 고유성은 문제가 되지 않았습니다. 고유성은 데이터 정의에 의해 달성되었습니다. 그러나 이 시점부터 프로그램은 미리 로드된 사용자 이미지로 작동해야 합니다. 그렇다면 원본 이미지를 기반으로 유사하지만 항상 고유한(바이트 및 결과적으로 해시 측면에서) 이미지를 어떻게 생성할 수 있습니까?


제가 제안하는 해결책은 아주 간단합니다. 이미지의 왼쪽 상단에 RGB 노이즈 사각형을 배치해 보겠습니다. 노이즈 사각형은 16x16픽셀이어야 합니다. 이는 거의 동일한 그림을 제공하지만 고유한 바이트 시퀀스를 보장합니다. 다양한 이미지를 보장하는 것만으로도 충분할까요? 수학을 좀 해보자. 정사각형의 크기는 16입니다. 단일 정사각형에 16×16 = 256 RGB 픽셀이 있습니다. 각 픽셀에는 256×256×256 = 16,777,216개의 변형이 있습니다.


따라서 고유 사각형의 수는 16,777,216^256입니다. 이는 1,558자리 숫자로, 관측 가능한 우주에 있는 원자 수보다 훨씬 많습니다. 그렇다면 정사각형 크기를 줄일 수 있다는 뜻인가요? 안타깝게도 JPEG와 같은 손실 압축은 고유한 사각형의 수를 크게 줄이므로 16x16이 최적의 크기입니다.


노이즈 사각형이 있는 이미지의 예

클래스에 대한 패스스루

나무
FUSE 기반 시스템의 인터페이스와 클래스를 보여주는 UML 클래스 다이어그램. IFUSEHandler, ObjectTreeNode 및 IFUSETreeNode 인터페이스를 포함하며 IFSETreeNode를 구현하는 FileFUSEtreeNode 및 DirectoryFUSETreeNode가 포함됩니다. 각 인터페이스와 클래스에는 속성과 메소드가 나열되어 관계와 계층 구조가 표시됩니다.

IFUSEHandler 일반적인 FUSE 호출을 제공하는 인터페이스입니다. read/write 각각 readAll/writeAll 로 대체한 것을 볼 수 있습니다. 읽기 및 쓰기 작업을 단순화하기 위해 이렇게 했습니다. IFUSEHandler 전체 부분에 대해 읽기/쓰기를 수행하면 부분 읽기/쓰기 논리를 다른 위치로 이동할 수 있습니다. 이는 IFUSEHandler 파일 설명자, 바이너리 데이터 등에 대해 알 필요가 없음을 의미합니다.


open FUSE 방법에서도 같은 일이 일어났습니다. 트리의 주목할만한 측면은 요청 시 생성된다는 것입니다. 전체 트리를 메모리에 저장하는 대신 프로그램은 액세스될 때만 노드를 생성합니다. 이 동작을 통해 프로그램은 노드 생성 또는 제거 시 트리 재구축 관련 문제를 방지할 수 있습니다.


ObjectTreeNode 인터페이스를 확인하면 children 배열이 아니라 메서드이므로 요청 시 자식이 생성되는 방식임을 알 수 있습니다. FileFUSETreeNodeDirectoryFUSETreeNode 일부 메서드에서 NotSupported 오류가 발생하는 추상 클래스입니다(분명히 FileFUSETreeNode readdir 구현해서는 안 됩니다).

FUSE외관

FUSE 시스템에 대한 인터페이스와 해당 관계를 보여주는 UML 클래스 다이어그램입니다. 다이어그램에는 IFUSEHandler, IFUSETreeNode, IFileDescriptorStorage 인터페이스 및 FUSEFacade 클래스가 포함되어 있습니다. IFUSEHandler에는 name 속성과 checkAvailability, create, getattr, readAll, Remove 및 writeAll 메소드가 있습니다. IFileDescriptorStorage에는 get, openRO, openWO 및 release 메소드가 있습니다. IFUSETreeNode는 IFUSEHandler를 확장합니다. FUSEFacade에는 생성자, create, getattr, open, read, readdir, release, rmdir, safeGetNode, unlink 및 write 메소드가 포함되어 있으며 IFUSETreeNode 및 IFileDescriptorStorage와 상호 작용합니다.


FUSEFacade는 프로그램의 주요 로직을 구현하고 다양한 부분을 하나로 묶는 가장 중요한 클래스입니다. node-fuse-bindings 콜백 기반 API가 있지만 FUSEFacade 메서드는 Promise 기반 API로 만들어집니다. 이러한 불편함을 해결하기 위해 다음과 같은 코드를 사용했습니다.

 const handleResultWrapper = <T>( promise: Promise<T>, cb: (err: number, result: T) => void, ) => { promise .then((result) => { cb(0, result); }) .catch((err) => { if (err instanceof FUSEError) { fuseLogger.info(`FUSE error: ${err}`); return cb(err.code, null as T); } fuseLogger.warn(err); cb(fuse.EIO, null as T); }); }; // Ex. usage: // open(path, flags, cb) { // handleResultWrapper(fuseFacade.open(path, flags), cb); // },


FUSEFacade 메서드는 handleResultWrapper 에 래핑됩니다. 경로를 사용하는 FUSEFacade 의 각 메서드는 단순히 경로를 구문 분석하고 트리에서 노드를 찾은 다음 요청된 메서드를 호출합니다.


FUSEFacade 클래스의 몇 가지 메서드를 고려해 보세요.

 async create(path: string, mode: number): Promise<number> { this.logger.info(`create(${path})`); // Convert path `/Image Manager/1/image.jpg` in // `['Image Manager', '1', 'image.jpg']` // splitPath will throw error if something goes wrong const parsedPath = this.splitPath(path); // `['Image Manager', '1', 'image.jpg']` const name = parsedPath.pop()!; // 'image.jpg' // Get node by path (`/Image Manager/1` after `pop` call) // or throw an error if node not found const node = await this.safeGetNode(parsedPath); // Call the IFUSEHandler method. Pass only a name, not a full path! await node.create(name, mode); // Create a file descriptor const fdObject = this.fdStorage.openWO(); return fdObject.fd; } async readdir(path: string): Promise<string[]> { this.logger.info(`readdir(${path})`); const node = await this.safeGetNode(path); // As you see, the tree is generated on the fly return (await node.children()).map((child) => child.name); } async open(path: string, flags: number): Promise<number> { this.logger.info(`open(${path}, ${flags})`); const node = await this.safeGetNode(path); // A leaf node is a directory if (!node.isLeaf) { throw new FUSEError(fuse.EACCES, 'invalid path'); } // Usually checkAvailability checks access await node.checkAvailability(flags); // Get node content and put it in created file descriptor const fileData: Buffer = await node.readAll(); // fdStorage is IFileDescriptorStorage, we will consider it below const fdObject = this.fdStorage.openRO(fileData); return fdObject.fd; }

파일 설명자

다음 단계를 진행하기 전에 프로그램의 맥락에서 파일 설명자가 무엇인지 자세히 살펴보겠습니다.

FUSE 시스템의 파일 설명자에 대한 인터페이스와 해당 관계를 보여주는 UML 클래스 다이어그램입니다. 다이어그램에는 IFileDescriptor, IFileDescriptorStorage 인터페이스, ReadWriteFileDescriptor, ReadFileDescriptor 및 WriteFileDescriptor 클래스가 포함되어 있습니다. IFileDescriptor에는 바이너리, fd, 크기 속성과 readToBuffer, writeToBuffer 메서드가 있습니다. IFileDescriptorStorage에는 get, openRO, openWO 및 release 메소드가 있습니다. ReadWriteFileDescriptor는 추가 생성자, readToBuffer 및 writeToBuffer 메서드를 사용하여 IFileDescriptor를 구현합니다. ReadFileDescriptor 및 WriteFileDescriptor는 ReadWriteFileDescriptor를 확장합니다. ReadFileDescriptor에는 writeToBuffer 메서드가 있고 WriteFileDescriptor에는 readToBuffer 메서드가 있습니다.

ReadWriteFileDescriptor 는 파일 설명자를 숫자로 저장하고 바이너리 데이터를 버퍼로 저장하는 클래스입니다. 클래스에는 파일 설명자 버퍼에 데이터를 읽고 쓸 수 있는 기능을 제공하는 readToBufferwriteToBuffer 메서드가 있습니다. ReadFileDescriptorWriteFileDescriptor 읽기 전용 및 쓰기 전용 설명자의 구현입니다.


IFileDescriptorStorage 는 파일 설명자 저장소를 설명하는 인터페이스입니다. 프로그램에는 이 인터페이스에 대한 구현이 하나만 있습니다: InMemoryFileDescriptorStorage . 이름에서 알 수 있듯이 설명자에 대한 지속성이 필요하지 않기 때문에 파일 설명자를 메모리에 저장합니다.


FUSEFacade 파일 설명자와 저장소를 어떻게 사용하는지 확인해 보겠습니다.

 async read( fd: number, // File descriptor to read from buf: Buffer, // Buffer to store the read data len: number, // Length of data to read pos: number, // Position in the file to start reading from ): Promise<number> { // Retrieve the file descriptor object from storage const fdObject = this.fdStorage.get(fd); if (!fdObject) { // If the file descriptor is invalid, throw an error throw new FUSEError(fuse.EBADF, 'invalid fd'); } // Read data into the buffer and return the number of bytes read return fdObject.readToBuffer(buf, len, pos); } async write( fd: number, // File descriptor to write to buf: Buffer, // Buffer containing the data to write len: number, // Length of data to write pos: number, // Position in the file to start writing at ): Promise<number> { // Retrieve the file descriptor object from storage const fdObject = this.fdStorage.get(fd); if (!fdObject) { // If the file descriptor is invalid, throw an error throw new FUSEError(fuse.EBADF, 'invalid fd'); } // Write data from the buffer and return the number of bytes written return fdObject.writeToBuffer(buf, len, pos); } async release(path: string, fd: number): Promise<0> { // Retrieve the file descriptor object from storage const fdObject = this.fdStorage.get(fd); if (!fdObject) { // If the file descriptor is invalid, throw an error throw new FUSEError(fuse.EBADF, 'invalid fd'); } // Safely get the node corresponding to the file path const node = await this.safeGetNode(path); // Write all the data from the file descriptor object to the node await node.writeAll(fdObject.binary); // Release the file descriptor from storage this.fdStorage.release(fd); // Return 0 indicating success return 0; }


위의 코드는 간단합니다. 파일 설명자를 읽고, 쓰고, 해제하는 메서드를 정의하여 작업을 수행하기 전에 파일 설명자가 유효한지 확인합니다. release 메소드는 또한 파일 설명자 객체의 데이터를 파일 시스템 노드에 쓰고 파일 설명자를 해제합니다.


libfuse 와 트리 주변의 코드는 끝났습니다. 이제 이미지 관련 코드를 살펴보겠습니다.

이미지: "데이터 전송 개체" 부분
이미지 처리를 위한 인터페이스와 해당 관계를 보여주는 UML 클래스 다이어그램입니다. 다이어그램에는 ImageBinary, ImageMeta, Image 및 IImageMetaStorage 인터페이스가 포함되어 있습니다. ImageBinary에는 버퍼와 크기 속성이 있습니다. ImageMeta에는 id, name, originalFileName 및 originalFileType 속성이 있습니다. 이미지에는 바이너리와 메타 속성이 있습니다. 여기서 바이너리는 ImageBinary 유형이고 메타는 ImageMeta 유형입니다. IImageMetaStorage에는 생성, 가져오기, 나열 및 제거 메서드가 있습니다.


ImageMeta 이미지에 대한 메타 정보를 저장하는 객체입니다. IImageMetaStorage 는 메타 저장소를 설명하는 인터페이스입니다. 프로그램에는 인터페이스에 대한 구현이 하나만 있습니다. FSImageMetaStorage 클래스는 IImageMetaStorage 인터페이스를 구현하여 단일 JSON 파일에 저장된 이미지 메타데이터를 관리합니다.


캐시를 사용하여 메모리에 메타데이터를 저장하고 필요할 때 JSON 파일을 읽어 캐시가 수화되도록 합니다. 이 클래스는 이미지 메타데이터를 생성, 검색, 나열 및 삭제하는 메서드를 제공하며 변경 사항을 JSON 파일에 다시 기록하여 업데이트를 유지합니다. 캐시는 IO 작업 횟수를 줄여 성능을 향상시킵니다.


ImageBinary 분명히 바이너리 이미지 데이터를 가지고 있는 객체입니다. Image 인터페이스는 ImageMetaImageBinary 의 조합입니다.

이미지: 바이너리 저장소 및 생성기

이미지 생성 및 바이너리 저장을 위한 인터페이스와 인터페이스의 관계를 보여주는 UML 클래스 다이어그램입니다. 다이어그램에는 IBinaryStorage, IImageGenerator 인터페이스, FSBinaryStorage, ImageGeneratorComposite, PassThroughImageGenerator, TextImageGenerator 및 ImageLoaderFacade 클래스가 포함되어 있습니다. IBinaryStorage에는 로드, 제거 및 쓰기 메소드가 있습니다. FSBinaryStorage는 IBinaryStorage를 구현하고 추가 생성자를 갖습니다. IImageGenerator에는 생성 메소드가 있습니다. PassThroughImageGenerator 및 TextImageGenerator는 IImageGenerator를 구현합니다. ImageGeneratorComposite에는 addGenerator 및 generate 메소드가 있습니다. ImageLoaderFacade에는 생성자와 로드 메서드가 있으며 IBinaryStorage 및 IImageGenerator와 상호 작용합니다.


IBinaryStorage 는 바이너리 데이터 저장을 위한 인터페이스입니다. 바이너리 저장소는 이미지와의 연결을 해제해야 하며 이미지, 비디오, JSON, 텍스트 등 모든 데이터를 저장할 수 있습니다. 이 사실은 우리에게 중요하며 그 이유를 알게 될 것입니다.


IImageGenerator 생성기를 설명하는 인터페이스입니다. 생성기는 프로그램의 중요한 부분입니다. 원시 바이너리 데이터와 메타 데이터를 가져와 이를 기반으로 이미지를 생성합니다. 프로그램에 생성기가 필요한 이유는 무엇입니까? 프로그램이 없어도 작동할 수 있나요?


가능하지만 생성기는 구현에 유연성을 추가합니다. 생성기를 사용하면 사용자는 그림, 텍스트 데이터 및 광범위하게 말하면 생성기를 작성하는 모든 데이터를 업로드할 수 있습니다.


IImageGenerator 인터페이스를 사용하여 텍스트 파일을 이미지로 변환하는 프로세스를 보여주는 다이어그램. 왼쪽에는 'Hello, world!'라는 내용이 포함된 'myfile.txt'라는 텍스트 파일에 대한 아이콘이 있습니다. 'IImageGenerator' 라벨이 붙은 화살표가 오른쪽을 가리키며, 여기에 'Hello, world!'라는 동일한 텍스트가 포함된 'myfile.png' 라벨이 붙은 이미지 파일에 대한 아이콘이 있습니다. 이미지에 표시된


흐름은 다음과 같습니다. 바이너리 데이터가 스토리지(위 그림의 myfile.txt )에서 로드된 다음 바이너리가 생성기로 전달됩니다. "즉시" 이미지를 생성합니다. 우리에게 더 편리한 한 형식에서 다른 형식으로의 변환기로 인식할 수 있습니다.


생성기의 예를 확인해 보겠습니다.

 import { createCanvas } from 'canvas'; // Import createCanvas function from the canvas library to create and manipulate images const IMAGE_SIZE_RE = /(\d+)x(\d+)/; // Regular expression to extract width and height dimensions from a string export class TextImageGenerator implements IImageGenerator { // method to generate an image from text async generate(meta: ImageMeta, rawBuffer: Buffer): Promise<Image | null> { // Step 1: Verify the MIME type is text if (meta.originalFileType !== MimeType.TXT) { // If the file type is not text, return null indicating no image generation return null; } // Step 2: Determine the size of the image const imageSize = { width: 800, // Default width height: 600, // Default height }; // Extract dimensions from the name if present const imageSizeRaw = IMAGE_SIZE_RE.exec(meta.name); if (imageSizeRaw) { // Update the width and height based on extracted values, or keep defaults imageSize.width = Number(imageSizeRaw[1]) || imageSize.width; imageSize.height = Number(imageSizeRaw[2]) || imageSize.height; } // Step 3: Convert the raw buffer to a string to get the text content const imageText = rawBuffer.toString('utf-8'); // Step 4: Create a canvas with the determined size const canvas = createCanvas(imageSize.width, imageSize.height); const ctx = canvas.getContext('2d'); // Get the 2D drawing context // Step 5: Prepare the canvas background ctx.fillStyle = '#000000'; // Set fill color to black ctx.fillRect(0, 0, imageSize.width, imageSize.height); // Fill the entire canvas with the background color // Step 6: Draw the text onto the canvas ctx.textAlign = 'start'; // Align text to the start (left) ctx.textBaseline = 'top'; // Align text to the top ctx.fillStyle = '#ffffff'; // Set text color to white ctx.font = '30px Open Sans'; // Set font style and size ctx.fillText(imageText, 10, 10); // Draw the text with a margin // Step 7: Convert the canvas to a PNG buffer and create the Image object return { meta, // Include the original metadata binary: { buffer: canvas.toBuffer('image/png'), // Convert canvas content to a PNG buffer }, }; } }


ImageLoaderFacade 클래스는 저장소와 생성기를 논리적으로 결합하는 Facade 입니다. 즉, 위에서 읽은 흐름을 구현합니다.

이미지: 변형

이미지 생성 및 바이너리 저장을 위한 인터페이스와 인터페이스의 관계를 보여주는 UML 클래스 다이어그램입니다. 다이어그램에는 IBinaryStorage, IImageGenerator 인터페이스, FSBinaryStorage, ImageGeneratorComposite, PassThroughImageGenerator, TextImageGenerator 및 ImageLoaderFacade 클래스가 포함되어 있습니다. IBinaryStorage에는 로드, 제거 및 쓰기 메소드가 있습니다. FSBinaryStorage는 IBinaryStorage를 구현하고 추가 생성자를 갖습니다. IImageGenerator에는 생성 메소드가 있습니다. PassThroughImageGenerator 및 TextImageGenerator는 IImageGenerator를 구현합니다. ImageGeneratorComposite에는 addGenerator 및 generate 메소드가 있습니다. ImageLoaderFacade에는 생성자와 로드 메서드가 있으며 IBinaryStorage 및 IImageGenerator와 상호 작용합니다.


IImageVariant 는 다양한 이미지 변형을 생성하기 위한 인터페이스입니다. 이러한 맥락에서 변형은 파일 시스템에서 파일을 볼 때 사용자에게 표시되는 "즉시" 생성된 이미지입니다. 생성기와의 주요 차이점은 원시 데이터가 아닌 이미지를 입력으로 사용한다는 것입니다.


이 프로그램에는 ImageAlwaysRandom , ImageOriginalVariantImageWithText 의 세 가지 변형이 있습니다. ImageAlwaysRandom 임의의 RGB 노이즈 사각형이 포함된 원본 이미지를 반환합니다.


 export class ImageAlwaysRandomVariant implements IImageVariant { // Define a constant for the size of the random square edge in pixels private readonly randomSquareEdgeSizePx = 16; // Constructor takes the desired output format for the image constructor(private readonly outputFormat: ImageFormat) {} // Asynchronous method to generate a random variant of an image async generate(image: Image): Promise<ImageBinary> { // Step 1: Load the image using the sharp library const sharpImage = sharp(image.binary.buffer); // Step 2: Retrieve metadata and raw buffer from the image const metadata = await sharpImage.metadata(); // Get image metadata const buffer = await sharpImage.raw().toBuffer(); // Get raw pixel data // the buffer size is plain array with size of image width * image height * channels count (3 or 4) // Step 3: Apply random pixel values to a small square region in the image for (let y = 0; y < this.randomSquareEdgeSizePx; y++) { for (let x = 0; x < this.randomSquareEdgeSizePx; x++) { // Calculate the buffer offset for the current pixel const offset = y * metadata.width! * metadata.channels! + x * metadata.channels!; // Set random values for RGB channels buffer[offset + 0] = randInt(0, 255); // Red channel buffer[offset + 1] = randInt(0, 255); // Green channel buffer[offset + 2] = randInt(0, 255); // Blue channel // If the image has an alpha channel, set it to 255 (fully opaque) if (metadata.channels === 4) { buffer[offset + 3] = 255; // Alpha channel } } } // Step 4: Create a new sharp image from the modified buffer and convert it to the desired format const result = await sharp(buffer, { raw: { width: metadata.width!, height: metadata.height!, channels: metadata.channels!, }, }) .toFormat(this.outputFormat) // Convert to the specified output format .toBuffer(); // Get the final image buffer // Step 5: Return the generated image binary data return { buffer: result, // Buffer containing the generated image }; } }


나는 NodeJS에서 이미지를 작업하는 가장 편리한 방법으로 sharp 라이브러리를 사용합니다: https://github.com/lovell/sharp .


ImageOriginalVariant 변경 없이 이미지를 반환합니다(그러나 다른 압축 형식으로 이미지를 반환할 수 있음). ImageWithText 위에 텍스트가 쓰여진 이미지를 반환합니다. 이는 단일 이미지의 사전 정의된 변형을 만들 때 도움이 될 것입니다. 예를 들어, 하나의 이미지에 10개의 무작위 변형이 필요한 경우 이러한 변형을 서로 구별해야 합니다.


여기서 해결 방법은 원본을 기반으로 10개의 그림을 만드는 것입니다. 여기서 각 이미지의 왼쪽 상단에 0부터 9까지의 일련 번호를 렌더링합니다.

넓은 눈을 가진 흰색과 검은색 고양이를 보여주는 일련의 이미지입니다. 이미지에는 왼쪽에서 0부터 시작하여 1씩 증가하고 오른쪽에서 9까지 타원으로 이어지는 숫자가 표시되어 있습니다. 고양이의 표정은 각 이미지에서 동일하게 유지됩니다.


ImageCacheWrapper 변형과 다른 목적을 가지며 특정 IImageVariant 클래스의 결과를 캐시하여 래퍼 역할을 합니다. 이미지 변환기, 텍스트-이미지 생성기 등과 같이 변경되지 않는 엔터티를 래핑하는 데 사용됩니다. 이 캐싱 메커니즘을 사용하면 주로 동일한 이미지를 여러 번 읽을 때 더 빠른 데이터 검색이 가능합니다.


글쎄, 우리는 프로그램의 모든 주요 부분을 다루었습니다. 이제 모든 것을 하나로 결합할 시간입니다.

트리 구조

이미지 관리와 관련된 다양한 FUSE 트리 노드 간의 계층 구조와 관계를 보여주는 UML 클래스 다이어그램입니다. 클래스에는 ImageVariantFileFUSETreeNode, ImageCacheWrapper, ImageItemAlwaysRandomDirFUSETreeNode, ImageItemOriginalDirFUSETreeNode, ImageItemCounterDirFUSETreeNode, ImageManagerItemFileFUSETreeNode, ImageItemDirFUSETreeNode, ImageManagerDirFUSETreeNode, ImagesDirFUSETreeNode 및 RootDirFUSETreeNode가 포함됩니다. 각 클래스에는 이미지 메타데이터, 바이너리 데이터 및 파일 작업(예: create, readAll, writeAll, 제거 및 getattr)과 관련된 속성 및 메서드가 있습니다.


아래 클래스 다이어그램은 트리 클래스가 해당 이미지 클래스와 결합되는 방식을 나타냅니다. 다이어그램은 아래에서 위로 읽어야 합니다. RootDir (이름에 FUSETreeNode 접미사는 사용하지 않음)는 프로그램이 구현하는 파일 시스템의 루트 디렉터리입니다. 위쪽 행으로 이동하면 ImagesDirImagesManagerDir 이라는 두 개의 디렉터리가 표시됩니다. ImagesManagerDir 에는 사용자 이미지 목록이 포함되어 있으며 이를 제어할 수 있습니다. 그런 다음 ImagesManagerItemFile 은 특정 파일에 대한 노드입니다. 이 클래스는 CRUD 작업을 구현합니다.


ImagesManagerDir을 노드의 일반적인 구현으로 고려하십시오.

 class ImageManagerDirFUSETreeNode extends DirectoryFUSETreeNode { name = 'Image Manager'; // Name of the directory constructor( private readonly imageMetaStorage: IImageMetaStorage, private readonly imageBinaryStorage: IBinaryStorage, ) { super(); // Call the parent class constructor } async children(): Promise<IFUSETreeNode[]> { // Dynamically create child nodes // In some cases, dynamic behavior can be problematic, requiring a cache of child nodes // to avoid redundant creation of IFUSETreeNode instances const list = await this.imageMetaStorage.list(); return list.map( (meta) => new ImageManagerItemFileFUSETreeNode( this.imageMetaStorage, this.imageBinaryStorage, meta, ), ); } async create(name: string, mode: number): Promise<void> { // Create a new image metadata entry await this.imageMetaStorage.create(name); } async getattr(): Promise<Stats> { return { // File modification date mtime: new Date(), // File last access date atime: new Date(), // File creation date // We do not store dates for our images, // so we simply return the current date ctime: new Date(), // Number of links nlink: 1, size: 100, // File access flags mode: FUSEMode.directory( FUSEMode.ALLOW_RWX, // Owner access rights FUSEMode.ALLOW_RX, // Group access rights FUSEMode.ALLOW_RX, // Access rights for all others ), // User ID of the file owner uid: process.getuid ? process.getuid() : 0, // Group ID for which the file is accessible gid: process.getgid ? process.getgid() : 0, }; } // Explicitly forbid deleting the 'Images Manager' folder remove(): Promise<void> { throw FUSEError.accessDenied(); } }


앞으로 ImagesDir 사용자 이미지의 이름을 딴 하위 디렉터리가 포함됩니다. ImagesItemDir 각 디렉토리를 담당합니다. 여기에는 사용 가능한 모든 변형이 포함됩니다. 기억하시겠지만 변형 수는 3개입니다. 각 변형은 다양한 형식(현재: jpeg, png 및 webm)의 최종 이미지 파일을 포함하는 디렉터리입니다. ImagesItemOriginalDirImagesItemCounterDir 생성된 모든 ImageVariantFile 인스턴스를 캐시에 래핑합니다.


인코딩은 CPU를 소모하므로 원본 이미지를 지속적으로 다시 인코딩하지 않으려면 이 작업이 필요합니다. 다이어그램 상단에는 ImageVariantFile 있습니다. 이는 앞서 설명한 IFUSEHandlerIImageVariant 구현 및 구성의 핵심입니다. 이것은 우리가 모든 노력을 기울여 만든 파일입니다.

테스트

최종 파일 시스템이 동일한 파일에 대한 병렬 요청을 어떻게 처리하는지 테스트해 보겠습니다. 이를 위해 다중 스레드에서 md5sum 유틸리티를 실행하여 파일 시스템에서 파일을 읽고 해당 해시를 계산합니다. 그런 다음 이 해시를 비교해 보겠습니다. 모든 것이 올바르게 작동한다면 해시가 달라야 합니다.

 #!/bin/bash # Loop to run the md5sum command 5 times in parallel for i in {1..5} do echo "Run $i..." # `&` at the end of the command runs it in the background md5sum ./mnt/Images/2020-09-10_22-43/always_random/2020-09-10_22-43.png & done echo 'wait...' # Wait for all background processes to finish wait


스크립트를 실행하고 다음 출력을 확인했습니다(명확성을 위해 약간 정리했습니다).

 Run 1... Run 2... Run 3... Run 4... Run 5... wait... bcdda97c480db74e14b8779a4e5c9d64 0954d3b204c849ab553f1f5106d576aa 564eeadfd8d0b3e204f018c6716c36e9 73a92c5ef27992498ee038b1f4cfb05e 77db129e37fdd51ef68d93416fec4f65


훌륭한! 모든 해시가 다르기 때문에 파일 시스템이 매번 고유한 이미지를 반환합니다.

결론

이 기사가 여러분이 FUSE 구현을 직접 작성하는 데 영감을 주었기를 바랍니다. 이 프로젝트의 소스 코드는 https://github.com/pinkiesky/node-fuse-images 에서 확인할 수 있습니다.


우리가 구축한 파일 시스템은 FUSE 및 Node.js 작업의 핵심 원칙을 보여주기 위해 단순화되었습니다. 예를 들어, 올바른 날짜를 고려하지 않습니다. 향상될 여지는 충분합니다. 사용자 GIF 파일에서 프레임 추출, 비디오 트랜스코딩, 심지어 작업자를 통한 작업 병렬화와 같은 기능을 추가한다고 상상해 보십시오.


그러나 완벽함은 선의 적입니다. 가지고 있는 것부터 시작하여 제대로 작동하게 한 다음 반복하세요. 즐거운 코딩하세요!