How to clone media files in server

Hi, I’m building a folders-files service with cut&copy&paste actions, and I need to create a custom controller to clone or duplicate media files in server. Keeping in mind that upload service can apply to any provider.

Any idea of how to begin?

System Information
  • strapi v4

Finally I adopted this solution:

/src/api/file/routes/01-custom.js:

module.exports = {
    routes: [
      {
        method: 'POST',
        path: '/files/clone/:id',
        handler: 'file.clone',
      }
    ]
  }

/src/api/file/controllers/file.js:

'use strict';
/**
 *  file controller
 */

const os = require('os');
const path = require('path');
const fs = require('fs');
const fse = require('fs-extra');
const http = require('http');
const https = require('https');
const FormData = require('form-data');
const fetch = require('node-fetch');

// 'name' is optional file name
async function urlDownload(url, name) {
  const tmpWorkingDirectory = await fse.mkdtemp(path.join(os.tmpdir(), 'strapi-upload-'));
  const file = fs.createWriteStream(tmpWorkingDirectory + "/" + (name || path.basename(url)))
  return new Promise((resolve, reject) => {
    const protocol = url.match('https://') ? https : http
    let req = protocol.get(url, function (response) {
      response.pipe(file);
      // after download completed close filestream
      file.on("finish", () => {
        file.close();
        resolve(file.path)
      });
    })
    req.on('error', err => {
      reject(err);
    });
  });
}

async function cloneMedia(strapi, ctx, url, name) {
  const {
    host,
    port
  } = strapi.config.get('server');
  const urlget = url.match(/^https?:\/\//) ? url : `http://${host}:${port}` + url
  const filePath = await urlDownload(urlget, name)
  const headers = {}
  const fields = ['authorization', 'Authorization']
  for (const key of fields)
    if (ctx.request.headers[key])
      headers[key] = ctx.request.headers[key]
  if (filePath) {
    const form = new FormData()
    form.append("files", fs.createReadStream(filePath))
    return await fetch(`http://${host}:${port}/api/upload`, {
        headers,
        method: 'POST',
        body: form,
      })
      .then(response => response.json())
  }
  return null
}

const {
  createCoreController
} = require('@strapi/strapi').factories;
module.exports = createCoreController('api::file.file',
  ({
    strapi
  }) => ({
    // clone file and related media
    async clone(ctx) {
      const {
        id,
      } = ctx.params      
      let file = await strapi.service("api::file.file").findOne(id, {
        populate: ['media']
      })
      if (!file)
        return ctx.notFound(`file ${id} not found`)
     
      const response = await cloneMedia(strapi, ctx, file.media.url, file.media.name)
        .catch(error => {
          return ctx.internalServerError(error.message)
        })
      if (response && !response.error) {
        delete file.id
        file.media = response[0].id
        const result = await strapi.service("api::file.file").create({
          data: file
        })
        return {
          data: result, meta:{}
        }
      }
      return {
        data: null,
        error: "Error in server"
      }
    }
  }))

Hi @msoler75 . I needed something similar to what you described and tried to implement your solution. But I keep getting this error:

{
  data: null,
  error: {
    status: 403,
    name: 'ForbiddenError',
    message: 'Forbidden',
    details: {}
  }
}

Have any idea?

I haven’t tested this setup, but it seems like you need to enable auth for your custom controller endpoint from settings.