๐Ÿ“ฆ vcgtz / ghost-gcp-storage-adapter

๐Ÿ“„ index.js ยท 105 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105const { Storage } = require('@google-cloud/storage');
const BaseAdapter = require('ghost-storage-base');
const path = require('path');
const URL = require('url').URL;

class GoogleCloudStorage extends BaseAdapter {
  constructor(config) {
    super();
    this.config = config;
    this.storage = new Storage({
      projectId: this.config.projectId,
      keyFilename: this.config.keyFilename
    });

    this.bucket = this.storage.bucket(this.config.bucketName);
  }

  async save(file, targetDir = this.getTargetDir()) {
    // Ensure target directory exists and is properly formatted
    targetDir = targetDir || this.getTargetDir();
    
    // Build complete file path
    const fileName = path.join(targetDir, file.name).replace(/\\/g, '/');

    const options = {
      destination: fileName,
      resumable: false,
      public: true,
      metadata: {
        cacheControl: `public, max-age=2678400`
      }
    };

    try {
      const [uploadedFile] = await this.bucket.upload(file.path, options);
      return uploadedFile.publicUrl();
    } catch (err) {
      console.error('Save error:', err);
      throw err;
    }
  }

  urlToPath(url) {
    try {
      // Parse URL
      const parsedUrl = new URL(url);
      
      let filePath;
      if (parsedUrl.hostname === 'storage.googleapis.com') {
        // Extract file path from URL, remove leading /bucketName/
        const pathParts = parsedUrl.pathname.split('/').filter(Boolean);
        // Remove first element (bucket name)
        pathParts.shift();
        // Decode URL-encoded path
        filePath = pathParts.map(part => decodeURIComponent(part)).join('/');
      } else {
        // For other cases, use pathname directly and remove leading slashes
        filePath = decodeURIComponent(parsedUrl.pathname.replace(/^\/+/, ''));
      }

      return filePath;
    } catch (err) {
      console.warn(`URL parsing failed for ${url}, falling back to basic string handling`);
      // If parsing fails, try basic string handling
      const urlWithoutQuery = decodeURIComponent(url.split('?')[0]);
      const matches = urlWithoutQuery.match(/storage\.googleapis\.com\/[^/]+\/(.+)$/);
      if (matches && matches[1]) {
        return matches[1];
      }
      return decodeURIComponent(urlWithoutQuery.split('/').pop());
    }
  }

  // Get target directory method
  getTargetDir(baseDir = '') {
    const date = new Date();
    const year = date.getFullYear();
    const month = (`0${date.getMonth() + 1}`).slice(-2);
    
    return path.join(baseDir, String(year), month);
  }

  exists(fileName, targetDir) {
    const filePath = path.join(targetDir || '', fileName).replace(/\\/g, '/');
    return this.bucket.file(filePath).exists().then(([exists]) => exists);
  }

  serve() {
    return (req, res, next) => next();
  }

  async delete(fileName, targetDir) {
    const filePath = path.join(targetDir || '', fileName).replace(/\\/g, '/');
    return await this.bucket.file(filePath).delete().catch((err) => {
      console.warn(`Delete failed for ${filePath}:`, err);
    });
  }

  read(options) {
    const filePath = options.path.replace(/\\/g, '/');
    return this.bucket.file(filePath).createReadStream();
  }
}

module.exports = GoogleCloudStorage;