Browse Source

feat: upload site favicon

Nicolas Giard 2 năm trước cách đây
mục cha
commit
9a4ac686ca

+ 8 - 0
server/controllers/common.js

@@ -51,6 +51,14 @@ router.get('/_site/:siteId?/:resource', async (req, res, next) => {
       }
       break
     }
+    case 'favicon': {
+      if (site.config.assets.favicon) {
+        res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
+      } else {
+        res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
+      }
+      break
+    }
     default: {
       return res.status(404).send('Invalid Site Resource')
     }

+ 2 - 1
server/db/migrations/3.0.0.js

@@ -488,7 +488,8 @@ exports.up = async knex => {
       assets: {
         logo: false,
         logoExt: 'svg',
-        favicon: false
+        favicon: false,
+        faviconExt: 'svg'
       },
       theme: {
         dark: false,

+ 49 - 4
server/graph/resolvers/site.js

@@ -156,6 +156,9 @@ module.exports = {
         if (!WIKI.extensions.ext.sharp.isInstalled) {
           throw new Error('This feature requires the Sharp extension but it is not installed.')
         }
+        if (!['.svg', '.png', '.jpg', 'webp', '.gif'].some(s => filename.endsWith(s))) {
+          throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
+        }
         const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
         const destFolder = path.resolve(
           process.cwd(),
@@ -198,10 +201,52 @@ module.exports = {
      * UPLOAD FAVICON
      */
     async uploadSiteFavicon (obj, args) {
-      const { filename, mimetype, createReadStream } = await args.image
-      console.info(filename, mimetype)
-      return {
-        operation: graphHelper.generateSuccess('Site favicon uploaded successfully')
+      try {
+        const { filename, mimetype, createReadStream } = await args.image
+        WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
+        if (!WIKI.extensions.ext.sharp.isInstalled) {
+          throw new Error('This feature requires the Sharp extension but it is not installed.')
+        }
+        if (!['.svg', '.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) {
+          throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
+        }
+        const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
+        const destFolder = path.resolve(
+          process.cwd(),
+          WIKI.config.dataPath,
+          `assets`
+        )
+        const destPath = path.join(destFolder, `favicon-${args.id}.${destFormat}`)
+        await fs.ensureDir(destFolder)
+        // -> Resize
+        await WIKI.extensions.ext.sharp.resize({
+          format: destFormat,
+          inputStream: createReadStream(),
+          outputPath: destPath,
+          width: 64,
+          height: 64
+        })
+        // -> Save favicon meta to DB
+        const site = await WIKI.models.sites.query().findById(args.id)
+        if (!site.config.assets.favicon) {
+          site.config.assets.favicon = uuid()
+        }
+        site.config.assets.faviconExt = destFormat
+        await WIKI.models.sites.query().findById(args.id).patch({ config: site.config })
+        await WIKI.models.sites.reloadCache()
+        // -> Save image data to DB
+        const imgBuffer = await fs.readFile(destPath)
+        await WIKI.models.knex('assetData').insert({
+          id: site.config.assets.favicon,
+          data: imgBuffer
+        }).onConflict('id').merge()
+        WIKI.logger.info('New site favicon processed successfully.')
+        return {
+          operation: graphHelper.generateSuccess('Site favicon uploaded successfully')
+        }
+      } catch (err) {
+        WIKI.logger.warn(err)
+        return graphHelper.generateError(err)
       }
     }
   }

+ 1 - 1
ux/index.html

@@ -2,7 +2,7 @@
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
-    <link rel="icon" href="/favicon.ico" />
+    <link rel="icon" href="/_site/favicon" />
     <meta name="format-detection" content="telephone=no">
     <meta name="msapplication-tap-highlight" content="no">
     <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

+ 6 - 0
ux/quasar.config.js

@@ -77,6 +77,12 @@ module.exports = configure(function (/* ctx */) {
 
       extendViteConf (viteConf) {
         viteConf.build.assetsDir = '_assets'
+        viteConf.build.rollupOptions = {
+          ...viteConf.build.rollupOptions ?? {},
+          external: [
+            /^\/_site\//
+          ]
+        }
       },
       // viteVuePluginOptions: {},
 

+ 2 - 2
ux/src/i18n/locales/en.json

@@ -173,14 +173,14 @@
   "admin.general.displaySiteTitle": "Display Site Title",
   "admin.general.displaySiteTitleHint": "Should the site title be displayed next to the logo? If your logo isn't square and contain your brand name, turn this option off.",
   "admin.general.favicon": "Favicon",
-  "admin.general.faviconHint": "Favicon image file, in SVG, PNG, ICO or GIF format. Must be a square image.",
+  "admin.general.faviconHint": "Favicon image file, in SVG, PNG, JPG, WEBP or GIF format. Must be a square image.",
   "admin.general.faviconUploadSuccess": "Site Favicon uploaded successfully.",
   "admin.general.features": "Features",
   "admin.general.footerCopyright": "Footer / Copyright",
   "admin.general.general": "General",
   "admin.general.logo": "Logo",
   "admin.general.logoUpl": "Site Logo",
-  "admin.general.logoUplHint": "Logo image file, in SVG, PNG, JPG or GIF format.",
+  "admin.general.logoUplHint": "Logo image file, in SVG, PNG, JPG, WEBP or GIF format.",
   "admin.general.logoUploadSuccess": "Site logo uploaded successfully.",
   "admin.general.ratingsOff": "Off",
   "admin.general.ratingsStars": "Stars",

+ 23 - 15
ux/src/pages/AdminGeneral.vue

@@ -302,10 +302,11 @@ q-page.admin-general
             .admin-general-favicontabs.q-mt-md
               div
                 q-avatar(
+                  v-if='adminStore.currentSiteId'
                   size='24px'
                   square
                   )
-                  img(src='/_assets/logo-wikijs.svg')
+                  img(:src='`/_site/` + adminStore.currentSiteId + `/favicon?` + state.assetTimestamp')
                 .text-caption.q-ml-sm {{state.config.title}}
               div
                 q-icon(name='las la-otter', size='24px', color='grey')
@@ -669,7 +670,7 @@ async function uploadLogo () {
   input.onchange = async e => {
     state.loading++
     try {
-      await APOLLO_CLIENT.mutate({
+      const resp = await APOLLO_CLIENT.mutate({
         mutation: gql`
           mutation uploadLogo (
             $id: UUID!
@@ -681,7 +682,6 @@ async function uploadLogo () {
             ) {
               operation {
                 succeeded
-                slug
                 message
               }
             }
@@ -692,11 +692,15 @@ async function uploadLogo () {
           image: e.target.files[0]
         }
       })
-      $q.notify({
-        type: 'positive',
-        message: t('admin.general.logoUploadSuccess')
-      })
-      state.assetTimestamp = (new Date()).toISOString()
+      if (resp?.data?.uploadSiteLogo?.operation?.succeeded) {
+        $q.notify({
+          type: 'positive',
+          message: t('admin.general.logoUploadSuccess')
+        })
+        state.assetTimestamp = (new Date()).toISOString()
+      } else {
+        throw new Error(resp?.data?.uploadSiteLogo?.operation?.message || 'An unexpected error occured.')
+      }
     } catch (err) {
       $q.notify({
         type: 'negative',
@@ -717,7 +721,7 @@ async function uploadFavicon () {
   input.onchange = async e => {
     state.loading++
     try {
-      await APOLLO_CLIENT.mutate({
+      const resp = await APOLLO_CLIENT.mutate({
         mutation: gql`
           mutation uploadFavicon (
             $id: UUID!
@@ -727,9 +731,8 @@ async function uploadFavicon () {
               id: $id
               image: $image
             ) {
-              status {
+              operation {
                 succeeded
-                slug
                 message
               }
             }
@@ -740,10 +743,15 @@ async function uploadFavicon () {
           image: e.target.files[0]
         }
       })
-      $q.notify({
-        type: 'positive',
-        message: t('admin.general.faviconUploadSuccess')
-      })
+      if (resp?.data?.uploadSiteFavicon?.operation?.succeeded) {
+        $q.notify({
+          type: 'positive',
+          message: t('admin.general.faviconUploadSuccess')
+        })
+        state.assetTimestamp = (new Date()).toISOString()
+      } else {
+        throw new Error(resp?.data?.uploadSiteFavicon?.operation?.message || 'An unexpected error occured.')
+      }
     } catch (err) {
       $q.notify({
         type: 'negative',

+ 1 - 1
ux/src/pages/AdminTerminal.vue

@@ -119,7 +119,7 @@ onMounted(() => {
   state.connecting = true
 
   // socket = io(window.location.host, {
-  socket = io('localhost:3000', {
+  socket = io(window.location.host, {
     path: '/_ws/',
     auth: {
       token: 'TEST' // TODO: Use active token