首页 公告 项目 RSS

Setting Up and Using Linkwarden

March 20, 2024 本文有 897 个字 需要花费 5 分钟阅读

  1. Introduction

As an enthusiast who loves reading and collecting online resources, I often face the challenge of managing a large number of URLs. I used to use TickTick to manage these URLs, but over time, some links may become invalid - a common occurrence on the Internet. I’ve known about the Linkwarden project for a long time, but it wasn’t mature back then. Recently, I revisited this project and found that it has matured enough to support browser extensions, iOS shortcut command import of URLs, and PWA. It can almost be said to be a perfect solution.

Setting up PostgreSQL

Linkwarden uses PostgreSQL for database storage. I usually deploy it in a Kubernetes environment. Here is an example of a PostgreSQL Kubernetes configuration:

First is sts.yaml:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgresql
  namespace: app
spec:
  selector:
    matchLabels:
      app: postgresql
  serviceName: postgresql
  replicas: 1
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      hostNetwork: true
      containers:
      - name: postgresql
        image: postgres:14.11-alpine3.19
        ports:
        - containerPort: 5432
          name: postgresql
        env:
        - name: POSTGRES_PASSWORD
          value: 'xxxxxx'
        - name: POSTGRES_HOST_AUTH_METHOD
          value: trust
        volumeMounts:
        - name: postgresql-data
          mountPath: /var/lib/postgresql/data
        - name: timezone
          mountPath: /etc/localtime
          readOnly: true
      volumes:
        - name: timezone
          hostPath: 
            path: /usr/share/zoneinfo/Asia/Shanghai  
  volumeClaimTemplates:
  - metadata:
      name: postgresql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 200Gi

Next is svc.yaml:

apiVersion: v1
kind: Service
metadata:
  name: postgresql
  namespace: app
spec:
  selector:
    app: postgresql
  ports:
  - port: 5432
    targetPort: 5432

Setting up Linkwarden

Next is an example of a Linkwarden Kubernetes configuration:

First is sts.yaml:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: linkwarden
  namespace: app
spec:
  selector:
    matchLabels:
      app: linkwarden
  serviceName: linkwarden
  replicas: 1
  template:
    metadata:
      labels:
        app: linkwarden
    spec:
      containers:
      - name: linkwarden
        image: ghcr.io/linkwarden/linkwarden:v2.5.1
        ports:
        - containerPort: 3000
          name: linkwarden
        env:
        - name: NEXTAUTH_SECRET
          value: 'xxxxxxxxxxxxxxx'
        - name: NEXTAUTH_URL
          value: https://linkwarden.your-domain.com/api/v1/auth
        - name: POSTGRES_PASSWORD
          value: 'xxxxxxxxxxxxxxxx'
        - name: DATABASE_URL
          value: postgresql://postgres:${POSTGRES_PASSWORD}@postgresql:5432/linkwarden
        volumeMounts:
        - name: linkwarden-data
          mountPath: /data/data
        - name: timezone
          mountPath: /etc/localtime
          readOnly: true
      volumes:
        - name: timezone
          hostPath: 
            path: /usr/share/zoneinfo/Asia/Shanghai  
  volumeClaimTemplates:
  - metadata:
      name: linkwarden-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 200Gi

Explanation of environment variables:

  • NEXTAUTH_SECRET: A random string, those who know me know that I would randomly create a folder and then get its MD5 as this random string.
  • NEXTAUTH_URL: The access URL of Linkwarden.
  • POSTGRES_PASSWORD: The password of the PostgreSQL database.
  • DATABASE_URL: The database connection address.

Other environment variables can be referred to the official Linkwarden documentation:

https://docs.linkwarden.app/self-hosting/environment-variables

Linkwarden also supports various third-party login methods, which can be referred to:

https://docs.linkwarden.app/self-hosting/sso-oauth

Next is svc.yaml:

apiVersion: v1
kind: Service
metadata:
  name: linkwarden
  namespace: app
spec:
  selector:
    app: linkwarden
  ports:
  - port: 3000
    targetPort: 3000

Finally, ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: linkwarden-ingress
  namespace: app
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  rules:
  - host: "your-domain.com"
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: linkwarden
            port:
              number: 3000
  tls:
  - hosts:
    - your-domain.com
    secretName: your-tls-secret

Exporting Data from TickTick

Since the number of URLs I’ve collected is close to 800, it’s obviously impractical to add them manually. Therefore, I need to first get historical data from TickTick. TickTick does not provide a data export function, but by analyzing network requests, I found that the following URL can be used to obtain all data:

https://api.ticktick.com/api/v2/batch/check/0

The returned data is in JSON format, where the projectProfiles key contains detailed information about the list, and the syncTaskBean key contains detailed information about the tasks. If you want to get all the data under a specific list, just convert the JSON data in syncTaskBean to CSV format, and then use Excel to filter the projectId field. The id in projectProfiles is the projectId in syncTaskBean.

After filtering, get the content of all title and content fields, and use a URL extraction tool (such as: https://www.67tool.com/text/extract-urls) to get all URLs.

Importing Data into Linkwarden

Next is the step of importing data. I wrote a Python script to complete this task. First, create a urls.txt file, and then write all the URLs obtained earlier line by line. Log in to Linkwarden, get your cookie after logging in, modify the cookie and corresponding Linkwarden address in the script below, and then run the script.

import requests
import json

# Read the URL in the text file
with open('urls.txt', 'r') as file:
    urls = file.readlines()

# Set request header
headers = {
    'authority': 'your-linkwarden-domain',
    'accept': '*/*',
    'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'content-type': 'application/json',
    'cookie': 'your-cookie', # Fill in your cookie
    'origin': 'https://your-linkwarden-domain',
    'referer': 'https://your-linkwarden-domain/dashboard',
    'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'sec-fetch-dest': 'empty',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'same-origin',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
}

# Traverse URL, send POST request
for url in urls:
    url = url.strip()
    data = {
        "name": "",
        "url": url, # Imported url
        "description": "",
        "type": "url",
        "tags": [],
        "preview": "",
        "image": "",
        "pdf": "",
        "readable": "",
        "textContent": "",
        "collection": {
            "id": "your-collection-id", # This is your linkwarden's collection id
            "name": "your-collection-name", # This is your linkwarden's collection name
            "ownerId": "your-user-id" # This is your linkwarden's user id
        }
    }

    response = requests.post('https://your-linkwarden-domain/api/v1/links', headers=headers, data=json.dumps(data))

    # Check response status
    if response.status_code == 200:
        print(f'Successfully posted URL: {url}')
    else:
        print(f'Failed to post URL: {url}. Status code: {response.status_code}')

Be sure to replace your-linkwarden-domain, your-cookie, your-collection-id, your-user-id, and your-collection-name in the script with your actual values.

In this way, all URLs have been successfully imported into Linkwarden.

Feel free to follow my blog at www.bboy.app

Have Fun