diff --git a/frontend/src/api/files.js b/frontend/src/api/files.js
index 7494e55b..6cae2359 100644
--- a/frontend/src/api/files.js
+++ b/frontend/src/api/files.js
@@ -1,4 +1,4 @@
-import { fetchURL, removePrefix } from "./utils";
+import { fetchURL, removePrefix, createURL } from "./utils";
 import { baseURL } from "@/utils/constants";
 import store from "@/store";
 
@@ -154,3 +154,33 @@ export async function checksum(url, algo) {
   const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
   return (await data.json()).checksums[algo];
 }
+
+export function getDownloadURL(file, inline) {
+  const params = {
+    ...(inline && { inline: "true" }),
+  };
+
+  return createURL("api/raw" + file.path, params);
+}
+
+export function getPreviewURL(file, size) {
+  const params = {
+    inline: "true",
+    key: Date.parse(file.modified),
+  };
+
+  return createURL("api/preview/" + size + file.path, params);
+}
+
+export function getSubtitlesURL(file) {
+  const params = {
+    inline: "true",
+  };
+
+  const subtitles = [];
+  for (const sub of file.subtitles) {
+    subtitles.push(createURL("api/raw" + sub, params));
+  }
+
+  return subtitles;
+}
diff --git a/frontend/src/api/pub.js b/frontend/src/api/pub.js
index 58eb1eb6..626571b3 100644
--- a/frontend/src/api/pub.js
+++ b/frontend/src/api/pub.js
@@ -1,4 +1,4 @@
-import { fetchURL, removePrefix } from "./utils";
+import { fetchURL, removePrefix, createURL } from "./utils";
 import { baseURL } from "@/utils/constants";
 
 export async function fetch(url, password = "") {
@@ -59,3 +59,12 @@ export function download(format, hash, token, ...files) {
 
   window.open(url);
 }
+
+export function getDownloadURL(share, inline = false) {
+  const params = {
+    ...(inline && { inline: "true" }),
+    ...(share.token && { token: share.token }),
+  };
+
+  return createURL("api/public/dl/" + share.hash + share.path, params, false);
+}
diff --git a/frontend/src/api/share.js b/frontend/src/api/share.js
index 54bbc460..29dfe877 100644
--- a/frontend/src/api/share.js
+++ b/frontend/src/api/share.js
@@ -1,4 +1,4 @@
-import { fetchURL, fetchJSON, removePrefix } from "./utils";
+import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
 
 export async function list() {
   return fetchJSON("/api/shares");
@@ -34,3 +34,7 @@ export async function create(url, password = "", expires = "", unit = "hours") {
     body: body,
   });
 }
+
+export function getShareURL(share) {
+  return createURL("share/" + share.hash, {}, false);
+}
diff --git a/frontend/src/api/utils.js b/frontend/src/api/utils.js
index 65c6740a..f9fc9023 100644
--- a/frontend/src/api/utils.js
+++ b/frontend/src/api/utils.js
@@ -1,6 +1,7 @@
 import store from "@/store";
 import { renew } from "@/utils/auth";
 import { baseURL } from "@/utils/constants";
+import { encodePath } from "@/utils/url";
 
 export async function fetchURL(url, opts) {
   opts = opts || {};
@@ -45,3 +46,18 @@ export function removePrefix(url) {
   if (url[0] !== "/") url = "/" + url;
   return url;
 }
+
+export function createURL(endpoint, params = {}, auth = true) {
+  const url = new URL(encodePath(endpoint), origin + baseURL);
+
+  const searchParams = {
+    ...(auth && { auth: store.state.jwt }),
+    ...params,
+  };
+
+  for (const key in searchParams) {
+    url.searchParams.set(key, searchParams[key]);
+  }
+
+  return url.toString();
+}
diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue
index 351fba1f..4495be82 100644
--- a/frontend/src/components/files/ListingItem.vue
+++ b/frontend/src/components/files/ListingItem.vue
@@ -35,7 +35,7 @@
 </template>
 
 <script>
-import { baseURL, enableThumbs } from "@/utils/constants";
+import { enableThumbs } from "@/utils/constants";
 import { mapMutations, mapGetters, mapState } from "vuex";
 import filesize from "filesize";
 import moment from "moment";
@@ -58,6 +58,7 @@ export default {
     "modified",
     "index",
     "readOnly",
+    "path",
   ],
   computed: {
     ...mapState(["user", "selected", "req", "jwt"]),
@@ -83,12 +84,12 @@ export default {
       return true;
     },
     thumbnailUrl() {
-      const path = this.url.replace(/^\/files\//, "");
+      const file = {
+        path: this.path,
+        modified: this.modified,
+      };
 
-      // reload the image when the file is replaced
-      const key = Date.parse(this.modified);
-
-      return `${baseURL}/api/preview/thumb/${path}?k=${key}&inline=true`;
+      return api.getPreviewURL(file, "thumb");
     },
     isThumbsEnabled() {
       return enableThumbs;
diff --git a/frontend/src/components/prompts/Share.vue b/frontend/src/components/prompts/Share.vue
index eaedafda..210b2f93 100644
--- a/frontend/src/components/prompts/Share.vue
+++ b/frontend/src/components/prompts/Share.vue
@@ -25,7 +25,7 @@
             <td class="small">
               <button
                 class="action copy-clipboard"
-                :data-clipboard-text="buildLink(link.hash)"
+                :data-clipboard-text="buildLink(link)"
                 :aria-label="$t('buttons.copyToClipboard')"
                 :title="$t('buttons.copyToClipboard')"
               >
@@ -118,7 +118,6 @@
 <script>
 import { mapState, mapGetters } from "vuex";
 import { share as api } from "@/api";
-import { baseURL } from "@/utils/constants";
 import moment from "moment";
 import Clipboard from "clipboard";
 
@@ -213,8 +212,8 @@ export default {
     humanTime(time) {
       return moment(time * 1000).fromNow();
     },
-    buildLink(hash) {
-      return `${window.location.origin}${baseURL}/share/${hash}`;
+    buildLink(share) {
+      return api.getShareURL(share);
     },
     sort() {
       this.links = this.links.sort((a, b) => {
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 9761339b..200c4e8d 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -14,6 +14,7 @@ const theme = window.FileBrowser.Theme;
 const enableThumbs = window.FileBrowser.EnableThumbs;
 const resizePreview = window.FileBrowser.ResizePreview;
 const enableExec = window.FileBrowser.EnableExec;
+const origin = window.location.origin;
 
 export {
   name,
@@ -31,4 +32,5 @@ export {
   enableThumbs,
   resizePreview,
   enableExec,
+  origin,
 };
diff --git a/frontend/src/utils/url.js b/frontend/src/utils/url.js
index 8346bb03..33d124a2 100644
--- a/frontend/src/utils/url.js
+++ b/frontend/src/utils/url.js
@@ -1,4 +1,4 @@
-function removeLastDir(url) {
+export function removeLastDir(url) {
   var arr = url.split("/");
   if (arr.pop() === "") {
     arr.pop();
@@ -9,7 +9,7 @@ function removeLastDir(url) {
 
 // this code borrow from mozilla
 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
-function encodeRFC5987ValueChars(str) {
+export function encodeRFC5987ValueChars(str) {
   return (
     encodeURIComponent(str)
       // Note that although RFC3986 reserves "!", RFC5987 does not,
@@ -22,7 +22,7 @@ function encodeRFC5987ValueChars(str) {
   );
 }
 
-function encodePath(str) {
+export function encodePath(str) {
   return str
     .split("/")
     .map((v) => encodeURIComponent(v))
@@ -30,7 +30,7 @@ function encodePath(str) {
 }
 
 export default {
-  encodeRFC5987ValueChars: encodeRFC5987ValueChars,
-  removeLastDir: removeLastDir,
-  encodePath: encodePath,
+  encodeRFC5987ValueChars,
+  removeLastDir,
+  encodePath,
 };
diff --git a/frontend/src/views/Share.vue b/frontend/src/views/Share.vue
index eefc6453..f26b4fc2 100644
--- a/frontend/src/views/Share.vue
+++ b/frontend/src/views/Share.vue
@@ -104,7 +104,7 @@
             </a>
           </div>
           <div class="share__box__element share__box__center">
-            <qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
+            <qrcode-vue :value="link" size="200" level="M"></qrcode-vue>
           </div>
         </div>
         <div
@@ -173,7 +173,6 @@
 <script>
 import { mapState, mapMutations, mapGetters } from "vuex";
 import { pub as api } from "@/api";
-import { baseURL } from "@/utils/constants";
 import filesize from "filesize";
 import moment from "moment";
 
@@ -231,21 +230,10 @@ export default {
       return "insert_drive_file";
     },
     link: function () {
-      let queryArg = "";
-      if (this.token !== "") {
-        queryArg = `?token=${this.token}`;
-      }
-
-      const path = this.$route.path.split("/").splice(2).join("/");
-      return `${baseURL}/api/public/dl/${path}${queryArg}`;
+      return api.getDownloadURL(this.req);
     },
     inlineLink: function () {
-      let url = new URL(this.fullLink);
-      url.searchParams.set("inline", "true");
-      return url.href;
-    },
-    fullLink: function () {
-      return window.location.origin + this.link;
+      return api.getDownloadURL(this.req, true);
     },
     humanSize: function () {
       if (this.req.isDir) {
@@ -287,6 +275,7 @@ export default {
 
       try {
         let file = await api.fetch(url, this.password);
+        file.hash = this.hash;
 
         this.token = file.token || "";
 
diff --git a/frontend/src/views/files/Listing.vue b/frontend/src/views/files/Listing.vue
index 4286e8dc..bdf4806b 100644
--- a/frontend/src/views/files/Listing.vue
+++ b/frontend/src/views/files/Listing.vue
@@ -209,6 +209,7 @@
             v-bind:modified="item.modified"
             v-bind:type="item.type"
             v-bind:size="item.size"
+            v-bind:path="item.path"
           >
           </item>
         </div>
@@ -225,6 +226,7 @@
             v-bind:modified="item.modified"
             v-bind:type="item.type"
             v-bind:size="item.size"
+            v-bind:path="item.path"
           >
           </item>
         </div>
diff --git a/frontend/src/views/files/Preview.vue b/frontend/src/views/files/Preview.vue
index 4e699dca..d08ac5aa 100644
--- a/frontend/src/views/files/Preview.vue
+++ b/frontend/src/views/files/Preview.vue
@@ -103,7 +103,7 @@
             </a>
             <a
               target="_blank"
-              :href="downloadUrl + '&inline=true'"
+              :href="raw"
               class="button button--flat"
               v-if="!req.isDir"
             >
@@ -145,7 +145,7 @@
 <script>
 import { mapState } from "vuex";
 import { files as api } from "@/api";
-import { baseURL, resizePreview } from "@/utils/constants";
+import { resizePreview } from "@/utils/constants";
 import url from "@/utils/url";
 import throttle from "lodash.throttle";
 import HeaderBar from "@/components/header/HeaderBar";
@@ -186,23 +186,14 @@ export default {
       return this.nextLink !== "";
     },
     downloadUrl() {
-      return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${
-        this.jwt
-      }`;
-    },
-    previewUrl() {
-      // reload the image when the file is replaced
-      const key = Date.parse(this.req.modified);
-
-      if (this.req.type === "image" && !this.fullSize) {
-        return `${baseURL}/api/preview/big${url.encodePath(
-          this.req.path
-        )}?k=${key}`;
-      }
-      return `${baseURL}/api/raw${url.encodePath(this.req.path)}?k=${key}`;
+      return api.getDownloadURL(this.req);
     },
     raw() {
-      return `${this.previewUrl}&inline=true`;
+      if (this.req.type === "image" && !this.fullSize) {
+        return api.getPreviewURL(this.req, "big");
+      }
+
+      return api.getDownloadURL(this.req, true);
     },
     showMore() {
       return this.$store.state.show === "more";
@@ -276,9 +267,7 @@ export default {
       }
 
       if (this.req.subtitles) {
-        this.subtitles = this.req.subtitles.map(
-          (sub) => `${baseURL}/api/raw${sub}?inline=true`
-        );
+        this.subtitles = api.getSubtitlesURL(this.req);
       }
 
       let dirs = this.$route.fullPath.split("/");
@@ -320,15 +309,14 @@ export default {
         return;
       }
     },
-    prefetchUrl: function (item) {
-      const key = Date.parse(item.modified);
-      if (item.type === "image" && !this.fullSize) {
-        return `${baseURL}/api/preview/big${item.path}?k=${key}&inline=true`;
-      } else if (item.type === "image") {
-        return `${baseURL}/api/raw${item.path}?k=${key}&inline=true`;
-      } else {
+    prefetchUrl(item) {
+      if (item.type !== "image") {
         return "";
       }
+
+      return this.fullSize
+        ? api.getDownloadURL(item, true)
+        : api.getPreviewURL(item, "big");
     },
     openMore() {
       this.$store.commit("showHover", "more");
@@ -358,7 +346,7 @@ export default {
       this.$router.push({ path: uri });
     },
     download() {
-      api.download(null, this.$route.path);
+      window.open(this.downloadUrl);
     },
   },
 };
diff --git a/frontend/src/views/settings/Shares.vue b/frontend/src/views/settings/Shares.vue
index b052c285..b27b4f7a 100644
--- a/frontend/src/views/settings/Shares.vue
+++ b/frontend/src/views/settings/Shares.vue
@@ -19,9 +19,7 @@
 
             <tr v-for="link in links" :key="link.hash">
               <td>
-                <a :href="buildLink(link.hash)" target="_blank">{{
-                  link.path
-                }}</a>
+                <a :href="buildLink(link)" target="_blank">{{ link.path }}</a>
               </td>
               <td>
                 <template v-if="link.expire !== 0">{{
@@ -43,7 +41,7 @@
               <td class="small">
                 <button
                   class="action copy-clipboard"
-                  :data-clipboard-text="buildLink(link.hash)"
+                  :data-clipboard-text="buildLink(link)"
                   :aria-label="$t('buttons.copyToClipboard')"
                   :title="$t('buttons.copyToClipboard')"
                 >
@@ -64,7 +62,6 @@
 
 <script>
 import { share as api, users } from "@/api";
-import { baseURL } from "@/utils/constants";
 import { mapState, mapMutations } from "vuex";
 import moment from "moment";
 import Clipboard from "clipboard";
@@ -136,8 +133,8 @@ export default {
     humanTime(time) {
       return moment(time * 1000).fromNow();
     },
-    buildLink(hash) {
-      return `${window.location.origin}${baseURL}/share/${hash}`;
+    buildLink(share) {
+      return api.getShareURL(share);
     },
   },
 };